Compare commits

..

2 Commits

Author SHA1 Message Date
d7b5b7e3ab Ченджлог (продолжение) 2024-11-25 18:11:46 +05:00
381557459b Ченджлог (начало) 2024-11-25 10:05:23 +05:00
48 changed files with 716 additions and 985 deletions

View File

@ -0,0 +1,61 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Persistence.Database.Model;
using Persistence.Models;
using Persistence.Repositories;
namespace Persistence.API.Controllers;
[ApiController]
[Authorize]
[Route("api/[controller]")]
public class ChangeLogRotorController : ControllerBase, IChangeLogApi<ProcessMapRotorDto, ChangeLogDto<ProcessMapRotorDto>>
{
private IChangeLogRepository<ProcessMapRotorDto, ChangeLogDto<ProcessMapRotorDto>> repository;
public ChangeLogRotorController(IChangeLogRepository<ProcessMapRotorDto, ChangeLogDto<ProcessMapRotorDto>> repository)
{
this.repository = repository;
}
public async Task<ActionResult<int>> Add(ProcessMapRotorDto dto, CancellationToken token)
{
await repository.InsertRange(0, [dto], token);
return null;
}
public Task<ActionResult<int>> AddRange(IEnumerable<ProcessMapRotorDto> dtos, CancellationToken token)
{
throw new NotImplementedException();
}
public Task<ActionResult<int>> Delete(int id, CancellationToken token)
{
throw new NotImplementedException();
}
public Task<ActionResult<int>> DeleteRange(IEnumerable<int> ids, CancellationToken token)
{
throw new NotImplementedException();
}
public Task<ActionResult<IEnumerable<ProcessMapRotorDto>>> GetChangeLogCurrent(CancellationToken token)
{
throw new NotImplementedException();
}
public Task<ActionResult<IEnumerable<ChangeLogDto<ProcessMapRotorDto>>>> GetChangeLogForDate(DateTimeOffset historyMoment, CancellationToken token)
{
throw new NotImplementedException();
}
public Task<ActionResult<int>> Update(ProcessMapRotorDto dto, CancellationToken token)
{
throw new NotImplementedException();
}
public Task<ActionResult<int>> UpdateRange(IEnumerable<ProcessMapRotorDto> dtos, CancellationToken token)
{
throw new NotImplementedException();
}
}

View File

@ -1,53 +0,0 @@
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
{
private readonly ISetpointRepository setpointRepository;
public SetpointController(ISetpointRepository setpointRepository)
{
this.setpointRepository = setpointRepository;
}
[HttpGet("current")]
public async Task<ActionResult<IEnumerable<SetpointValueDto>>> GetCurrent([FromQuery] IEnumerable<Guid> setpointKeys, CancellationToken token)
{
var result = await setpointRepository.GetCurrent(setpointKeys, token);
return Ok(result);
}
[HttpGet("history")]
public async Task<ActionResult<IEnumerable<SetpointValueDto>>> GetHistory([FromQuery] IEnumerable<Guid> setpointKeys, [FromQuery] DateTimeOffset historyMoment, CancellationToken token)
{
var result = await setpointRepository.GetHistory(setpointKeys, historyMoment, token);
return Ok(result);
}
[HttpGet("log")]
public async Task<ActionResult<Dictionary<Guid, IEnumerable<SetpointLogDto>>>> GetLog([FromQuery] IEnumerable<Guid> setpointKeys, CancellationToken token)
{
var result = await setpointRepository.GetLog(setpointKeys, token);
return Ok(result);
}
[HttpPost]
public async Task<ActionResult<int>> Save(Guid setpointKey, object newValue, CancellationToken token)
{
// ToDo: вычитка idUser
await setpointRepository.Save(setpointKey, newValue, 0, token);
return Ok();
}
}
}

View File

@ -1,10 +1,8 @@
using System.Text.Json.Nodes; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models;
using Persistence.Models.Configurations; using System.Text.Json.Nodes;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Persistence.API; namespace Persistence.API;
@ -32,164 +30,59 @@ public static class DependencyInjection
}); });
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Persistence web api", Version = "v1" }); 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"]),
}
}
});
var needUseKeyCloak = configuration.GetSection("NeedUseKeyCloak").Get<bool>(); c.AddSecurityRequirement(new OpenApiSecurityRequirement()
if (needUseKeyCloak) {
c.AddKeycloackSecurity(configuration); {
else c.AddDefaultSecurity(configuration); new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Keycloack"
},
Scheme = "Bearer",
Name = "Bearer",
In = ParameterLocation.Header,
},
new List<string>()
}
});
//var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; //var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
//var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); //var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
//var includeControllerXmlComment = true; //var includeControllerXmlComment = true;
//options.IncludeXmlComments(xmlPath, includeControllerXmlComment); //c.IncludeXmlComments(xmlPath, includeControllerXmlComment);
//options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "AsbCloudApp.xml"), includeControllerXmlComment); //c.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "AsbCloudApp.xml"), includeControllerXmlComment);
}); });
} }
#region Authentication public static void AddJWTAuthentication(this IServiceCollection services, IConfiguration configuration)
public static void AddJWTAuthentication(this IServiceCollection services, IConfiguration configuration)
{ {
var needUseKeyCloak = configuration services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.GetSection("NeedUseKeyCloak") .AddJwtBearer(o =>
.Get<bool>(); {
if (needUseKeyCloak) o.RequireHttpsMetadata = false;
services.AddKeyCloakAuthentication(configuration); o.Audience = configuration["Authentication:Audience"];
else services.AddDefaultAuthentication(configuration); o.MetadataAddress = configuration["Authentication:MetadataAddress"]!;
} o.TokenValidationParameters = new TokenValidationParameters
{
private static void AddKeyCloakAuthentication(this IServiceCollection services, IConfiguration configuration) ValidIssuer = configuration["Authentication:ValidIssuer"],
{ };
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) });
.AddJwtBearer(options => }
{
options.RequireHttpsMetadata = false;
options.Audience = configuration["Authentication:Audience"];
options.MetadataAddress = configuration["Authentication:MetadataAddress"]!;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = configuration["Authentication:ValidIssuer"],
};
});
}
private static void AddDefaultAuthentication(this IServiceCollection services, IConfiguration configuration)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = JwtParams.Issuer,
ValidateAudience = true,
ValidAudience = JwtParams.Audience,
ValidateLifetime = true,
IssuerSigningKey = JwtParams.SecurityKey,
ValidateIssuerSigningKey = false
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Headers["Authorization"]
.ToString()
.Replace(JwtBearerDefaults.AuthenticationScheme, string.Empty)
.Trim();
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<AuthUser>()!;
if (username != keyCloakUser.Username || password != keyCloakUser.Password)
{
context.Fail("username or password did not match");
}
return Task.CompletedTask;
}
};
});
}
#endregion
#region Security (Swagger)
private static void AddKeycloackSecurity(this SwaggerGenOptions options, IConfiguration configuration)
{
options.AddSecurityDefinition("Keycloack", new OpenApiSecurityScheme
{
Description = @"JWT Authorization header using the Bearer scheme. Enter 'Bearer' [space] and then your token in the text input below. Example: 'Bearer 12345abcdef'",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows
{
Implicit = new OpenApiOAuthFlow
{
AuthorizationUrl = new Uri(configuration["Authentication:AuthorizationUrl"]),
}
}
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Keycloack"
},
Scheme = "Bearer",
Name = "Bearer",
In = ParameterLocation.Header,
},
new List<string>()
}
});
}
private static void AddDefaultSecurity(this SwaggerGenOptions options, IConfiguration configuration)
{
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = @"JWT Authorization header using the Bearer scheme. Enter 'Bearer' [space] and then your token in the text input below. Example: 'Bearer 12345abcdef'",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer",
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
},
Scheme = "oauth2",
Name = "Bearer",
In = ParameterLocation.Header,
},
new List<string>()
}
});
}
#endregion
} }

View File

@ -8,7 +8,7 @@
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
}, },
"dotnetRunMessages": true, "dotnetRunMessages": true,
"applicationUrl": "http://localhost:13616" "applicationUrl": "http://localhost:5032"
}, },
"IIS Express": { "IIS Express": {
"commandName": "IISExpress", "commandName": "IISExpress",

View File

@ -4,6 +4,5 @@
"Default": "Information", "Default": "Information",
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
} }
}, }
"NeedUseKeyCloak": false
} }

View File

@ -1,12 +1,11 @@
{ {
"DbConnection": { "DbConnection": {
"Host": "localhost", "Host": "localhost",
"Port": 5432, "Port": 5432,
"Username": "postgres", "Username": "postgres",
"Password": "q" "Password": "q"
}, },
"NeedUseKeyCloak": false, "KeycloakTestUser": {
"AuthUser": {
"username": "myuser", "username": "myuser",
"password": 12345, "password": 12345,
"clientId": "webapi", "clientId": "webapi",

View File

@ -1,24 +0,0 @@
using Persistence.Models;
using Refit;
namespace Persistence.Client.Clients;
/// <summary>
/// Интерфейс для тестирования API, предназначенного для работы с уставками
/// </summary>
public interface ISetpointClient
{
private const string BaseRoute = "/api/setpoint";
[Get($"{BaseRoute}/current")]
Task<IApiResponse<IEnumerable<SetpointValueDto>>> GetCurrent([Query(CollectionFormat.Multi)] IEnumerable<Guid> setpointKeys);
[Get($"{BaseRoute}/history")]
Task<IApiResponse<IEnumerable<SetpointValueDto>>> GetHistory([Query(CollectionFormat.Multi)] IEnumerable<Guid> setpointKeys, [Query] DateTimeOffset historyMoment);
[Get($"{BaseRoute}/log")]
Task<IApiResponse<Dictionary<Guid, IEnumerable<SetpointLogDto>>>> GetLog([Query(CollectionFormat.Multi)] IEnumerable<Guid> setpointKeys);
[Post($"{BaseRoute}/")]
Task<IApiResponse> Save(Guid setpointKey, object newValue);
}

View File

@ -1,72 +0,0 @@
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 void Authorize(this HttpClient httpClient, IConfiguration configuration)
{
var authUser = configuration
.GetSection(nameof(AuthUser))
.Get<AuthUser>()!;
var needUseKeyCloak = configuration
.GetSection("NeedUseKeyCloak")
.Get<bool>()!;
var keycloakGetTokenUrl = configuration.GetSection("KeycloakGetTokenUrl").Get<string>() ?? string.Empty;
var jwtToken = needUseKeyCloak
? authUser.CreateKeyCloakJwtToken(keycloakGetTokenUrl)
: authUser.CreateDefaultJwtToken();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwtToken);
}
private static string CreateDefaultJwtToken(this AuthUser authUser)
{
var claims = new List<Claim>()
{
new("client_id", authUser.ClientId),
new("username", authUser.Username),
new("password", authUser.Password),
new("grant_type", authUser.GrantType)
};
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);
}
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<JwtToken>(keyCloackResponse.Content)!;
return token.AccessToken;
}
return String.Empty;
}
}

View File

@ -1,25 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.2.1" />
<PackageReference Include="Refit" Version="8.0.0" />
<PackageReference Include="Refit.HttpClientFactory" Version="8.0.0" />
<PackageReference Include="RestSharp" Version="112.1.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Persistence\Persistence.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Models\" />
</ItemGroup>
</Project>

View File

@ -1,32 +0,0 @@
using System.Text.Json;
using Microsoft.Extensions.Configuration;
using Persistence.Client.Helpers;
using Refit;
namespace Persistence.Client
{
/// <summary>
/// Фабрика клиентов для доступа к Persistence - сервису
/// </summary>
public class PersistenceClientFactory
{
private static readonly JsonSerializerOptions JsonSerializerOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true
};
private static readonly RefitSettings RefitSettings = new(new SystemTextJsonContentSerializer(JsonSerializerOptions));
private HttpClient httpClient;
public PersistenceClientFactory(IHttpClientFactory httpClientFactory, IConfiguration configuration)
{
this.httpClient = httpClientFactory.CreateClient();
httpClient.Authorize(configuration);
}
public T GetClient<T>()
{
return RestService.For<T>(httpClient, RefitSettings);
}
}
}

View File

@ -1,36 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Persistence.Database.Postgres.Migrations
{
/// <inheritdoc />
public partial class SetpointMigration : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Setpoint",
columns: table => new
{
Key = table.Column<Guid>(type: "uuid", nullable: false, comment: "Ключ"),
Created = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, comment: "Дата изменения уставки"),
Value = table.Column<object>(type: "jsonb", nullable: false, comment: "Значение уставки"),
IdUser = table.Column<int>(type: "integer", nullable: false, comment: "Id автора последнего изменения")
},
constraints: table =>
{
table.PrimaryKey("PK_Setpoint", x => new { x.Key, x.Created });
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Setpoint");
}
}
}

View File

@ -12,8 +12,8 @@ using Persistence.Database.Model;
namespace Persistence.Database.Postgres.Migrations namespace Persistence.Database.Postgres.Migrations
{ {
[DbContext(typeof(PersistenceDbContext))] [DbContext(typeof(PersistenceDbContext))]
[Migration("20241118052225_SetpointMigration")] [Migration("20241122124437_AddChangeLog")]
partial class SetpointMigration partial class AddChangeLog
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -27,14 +27,48 @@ namespace Persistence.Database.Postgres.Migrations
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "adminpack"); NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "adminpack");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Persistence.Database.Model.ChangeLog", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("Id");
b.Property<DateTimeOffset>("Creation")
.HasColumnType("timestamp with time zone")
.HasColumnName("Creation");
b.Property<int>("IdAuthor")
.HasColumnType("integer")
.HasColumnName("IdAuthor");
b.Property<int?>("IdEditor")
.HasColumnType("integer")
.HasColumnName("IdEditor");
b.Property<int?>("IdNext")
.HasColumnType("integer")
.HasColumnName("IdNext");
b.Property<DateTimeOffset?>("Obsolete")
.HasColumnType("timestamp with time zone")
.HasColumnName("Obsolete");
b.Property<object>("Value")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("Value");
b.HasKey("Id");
b.ToTable("ChangeLog");
});
modelBuilder.Entity("Persistence.Database.Model.DataSaub", b => modelBuilder.Entity("Persistence.Database.Model.DataSaub", b =>
{ {
b.Property<int>("Id") b.Property<DateTimeOffset>("Date")
.ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone")
.HasColumnType("integer") .HasColumnName("date");
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<double?>("AxialLoad") b.Property<double?>("AxialLoad")
.HasColumnType("double precision") .HasColumnType("double precision")
@ -100,10 +134,6 @@ namespace Persistence.Database.Postgres.Migrations
.HasColumnType("double precision") .HasColumnType("double precision")
.HasColumnName("rotorTorque"); .HasColumnName("rotorTorque");
b.Property<int>("TimeStamp")
.HasColumnType("integer")
.HasColumnName("timestamp");
b.Property<string>("User") b.Property<string>("User")
.HasColumnType("text") .HasColumnType("text")
.HasColumnName("user"); .HasColumnName("user");
@ -112,34 +142,10 @@ namespace Persistence.Database.Postgres.Migrations
.HasColumnType("double precision") .HasColumnType("double precision")
.HasColumnName("wellDepth"); .HasColumnName("wellDepth");
b.HasKey("Id"); b.HasKey("Date");
b.ToTable("DataSaub"); b.ToTable("DataSaub");
}); });
modelBuilder.Entity("Persistence.Database.Model.Setpoint", b =>
{
b.Property<Guid>("Key")
.HasColumnType("uuid")
.HasComment("Ключ");
b.Property<DateTimeOffset>("Created")
.HasColumnType("timestamp with time zone")
.HasComment("Дата изменения уставки");
b.Property<int>("IdUser")
.HasColumnType("integer")
.HasComment("Id автора последнего изменения");
b.Property<object>("Value")
.IsRequired()
.HasColumnType("jsonb")
.HasComment("Значение уставки");
b.HasKey("Key", "Created");
b.ToTable("Setpoint");
});
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }
} }

View File

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

View File

@ -24,6 +24,43 @@ namespace Persistence.Database.Postgres.Migrations
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "adminpack"); NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "adminpack");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Persistence.Database.Model.ChangeLog", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("Id");
b.Property<DateTimeOffset>("Creation")
.HasColumnType("timestamp with time zone")
.HasColumnName("Creation");
b.Property<int>("IdAuthor")
.HasColumnType("integer")
.HasColumnName("IdAuthor");
b.Property<int?>("IdEditor")
.HasColumnType("integer")
.HasColumnName("IdEditor");
b.Property<int?>("IdNext")
.HasColumnType("integer")
.HasColumnName("IdNext");
b.Property<DateTimeOffset?>("Obsolete")
.HasColumnType("timestamp with time zone")
.HasColumnName("Obsolete");
b.Property<object>("Value")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("Value");
b.HasKey("Id");
b.ToTable("ChangeLog");
});
modelBuilder.Entity("Persistence.Database.Model.DataSaub", b => modelBuilder.Entity("Persistence.Database.Model.DataSaub", b =>
{ {
b.Property<DateTimeOffset>("Date") b.Property<DateTimeOffset>("Date")
@ -106,30 +143,6 @@ namespace Persistence.Database.Postgres.Migrations
b.ToTable("DataSaub"); b.ToTable("DataSaub");
}); });
modelBuilder.Entity("Persistence.Database.Model.Setpoint", b =>
{
b.Property<Guid>("Key")
.HasColumnType("uuid")
.HasComment("Ключ");
b.Property<DateTimeOffset>("Created")
.HasColumnType("timestamp with time zone")
.HasComment("Дата изменения уставки");
b.Property<int>("IdUser")
.HasColumnType("integer")
.HasComment("Id автора последнего изменения");
b.Property<object>("Value")
.IsRequired()
.HasColumnType("jsonb")
.HasComment("Значение уставки");
b.HasKey("Key", "Created");
b.ToTable("Setpoint");
});
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }
} }

View File

@ -1,12 +1,11 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Data.Common; using System.Data.Common;
namespace Persistence.Database.Model; namespace Persistence.Database.Model;
public partial class PersistenceDbContext : DbContext, IPersistenceDbContext public partial class PersistenceDbContext : DbContext, IPersistenceDbContext
{ {
public DbSet<DataSaub> DataSaub => Set<DataSaub>(); public DbSet<DataSaub> DataSaub => Set<DataSaub>();
public DbSet<ChangeLog> ChangeLog => Set<ChangeLog>();
public DbSet<Setpoint> Setpoint => Set<Setpoint>();
public PersistenceDbContext() public PersistenceDbContext()
: base() : base()

View File

@ -0,0 +1,59 @@

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

View File

@ -0,0 +1,29 @@

using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;
namespace Persistence.Database.Model;
/// <summary>
/// Часть записи, описывающая изменение данных, содержащие начальную и конечную глубину, а также секцию
/// </summary>
public class ChangeLogWithWellDepthAndSectionId : ChangeLog
{
/// <summary>
/// Глубина забоя на дату начала интервала
/// </summary>
[Column("DepthStart")]
public double? DepthStart { get; set; }
/// <summary>
/// Глубина забоя на дату окончания интервала
/// </summary>
[Column("DepthEnd")]
public double? DepthEnd { get; set; }
/// <summary>
/// Ключ секции
/// </summary>
[Column("IdSection")]
public Guid IdSection { get; set; }
}

View File

@ -0,0 +1,43 @@

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

View File

@ -1,21 +0,0 @@
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace Persistence.Database.Model
{
[PrimaryKey(nameof(Key), nameof(Created))]
public class Setpoint
{
[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; }
}
}

View File

@ -5,4 +5,5 @@ namespace Persistence.Database;
public interface IPersistenceDbContext : IDisposable public interface IPersistenceDbContext : IDisposable
{ {
DbSet<DataSaub> DataSaub { get; } DbSet<DataSaub> DataSaub { get; }
DbSet<ChangeLog> ChangeLog { get; }
} }

View File

@ -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> DataSaub { get; }
DbSet<Setpoint> Setpoint { get; }
DatabaseFacade Database { get; }
Task<int> SaveChangesAsync(CancellationToken cancellationToken);
}

View File

@ -0,0 +1,43 @@
namespace Persistence.IntegrationTests;
public static class ApiTokenHelper
{
//public static string GetAdminUserToken()
//{
// var user = new User()
// {
// Id = 1,
// IdCompany = 1,
// Login = "test_user"
// };
// var roles = new[] { "root" };
// return CreateToken(user, roles);
//}
//private static string CreateToken(User user, IEnumerable<string> roles)
//{
// var claims = new List<Claim>
// {
// new("id", user.Id.ToString()),
// new(ClaimsIdentity.DefaultNameClaimType, user.Login),
// new("idCompany", user.IdCompany.ToString()),
// };
// claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));
// const string secret = "супер секретный ключ для шифрования";
// var key = Encoding.ASCII.GetBytes(secret);
// var tokenDescriptor = new SecurityTokenDescriptor
// {
// Issuer = "a",
// Audience = "a",
// Subject = new ClaimsIdentity(claims),
// Expires = DateTime.UtcNow.AddHours(1),
// SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
// };
// var tokenHandler = new JwtSecurityTokenHandler();
// var token = tokenHandler.CreateToken(tokenDescriptor);
// return tokenHandler.WriteToken(token);
//}
}

View File

@ -2,7 +2,7 @@
using Persistence.Models; using Persistence.Models;
using Refit; using Refit;
namespace Persistence.Client.Clients; namespace Persistence.IntegrationTests.Clients;
public interface ITimeSeriesClient<TDto> public interface ITimeSeriesClient<TDto>
where TDto : class, new() where TDto : class, new()
{ {

View File

@ -1,5 +1,4 @@
using Persistence.Client; using Persistence.Database.Model;
using Persistence.Database.Model;
using Persistence.Repository.Data; using Persistence.Repository.Data;
using Xunit; using Xunit;

View File

@ -1,159 +0,0 @@
using System.Net;
using Microsoft.Extensions.DependencyInjection;
using Persistence.Client;
using Persistence.Client.Clients;
using Xunit;
namespace Persistence.IntegrationTests.Controllers
{
public class SetpointControllerTest : BaseIntegrationTest
{
private ISetpointClient setpointClient;
private class TestObject
{
public string? value1 { get; set; }
public int? value2 { get; set; }
}
public SetpointControllerTest(WebAppFactoryFixture factory) : base(factory)
{
var scope = factory.Services.CreateScope();
var persistenceClientFactory = scope.ServiceProvider
.GetRequiredService<PersistenceClientFactory>();
setpointClient = persistenceClientFactory.GetClient<ISetpointClient>();
}
[Fact]
public async Task GetCurrent_returns_success()
{
//arrange
var setpointKeys = new List<Guid>()
{
Guid.NewGuid(),
Guid.NewGuid()
};
//act
var response = await setpointClient.GetCurrent(setpointKeys);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
Assert.Empty(response.Content);
}
[Fact]
public async Task GetCurrent_AfterSave_returns_success()
{
//arrange
var setpointKey = await Save();
//act
var response = await setpointClient.GetCurrent([setpointKey]);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
Assert.NotEmpty(response.Content);
Assert.Equal(setpointKey, response.Content.FirstOrDefault()?.Key);
}
[Fact]
public async Task GetHistory_returns_success()
{
//arrange
var setpointKeys = new List<Guid>()
{
Guid.NewGuid(),
Guid.NewGuid()
};
var historyMoment = DateTimeOffset.UtcNow;
//act
var response = await setpointClient.GetHistory(setpointKeys, historyMoment);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
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 setpointClient.GetHistory([setpointKey], historyMoment);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
Assert.NotEmpty(response.Content);
Assert.Equal(setpointKey, response.Content.FirstOrDefault()?.Key);
}
[Fact]
public async Task GetLog_returns_success()
{
//arrange
var setpointKeys = new List<Guid>()
{
Guid.NewGuid(),
Guid.NewGuid()
};
//act
var response = await setpointClient.GetLog(setpointKeys);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
Assert.Empty(response.Content);
}
[Fact]
public async Task GetLog_AfterSave_returns_success()
{
//arrange
var setpointKey = await Save();
//act
var response = await setpointClient.GetLog([setpointKey]);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Content);
Assert.NotEmpty(response.Content);
Assert.Equal(setpointKey, response.Content.FirstOrDefault().Key);
}
[Fact]
public async Task Save_returns_success()
{
await Save();
}
private async Task<Guid> Save()
{
//arrange
var setpointKey = Guid.NewGuid();
var setpointValue = new TestObject()
{
value1 = "1",
value2 = 2
};
//act
var response = await setpointClient.Save(setpointKey, setpointValue);
//assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
return setpointKey;
}
}
}

View File

@ -1,9 +1,7 @@
using System.Net; using Mapster;
using Mapster;
using Microsoft.Extensions.DependencyInjection;
using Persistence.Client;
using Persistence.Client.Clients;
using Persistence.Database.Model; using Persistence.Database.Model;
using Persistence.IntegrationTests.Clients;
using System.Net;
using Xunit; using Xunit;
namespace Persistence.IntegrationTests.Controllers; namespace Persistence.IntegrationTests.Controllers;
@ -11,17 +9,16 @@ public abstract class TimeSeriesBaseControllerTest<TEntity, TDto> : BaseIntegrat
where TEntity : class, ITimestampedData, new() where TEntity : class, ITimestampedData, new()
where TDto : class, new() where TDto : class, new()
{ {
private ITimeSeriesClient<TDto> timeSeriesClient; private ITimeSeriesClient<TDto> client;
public TimeSeriesBaseControllerTest(WebAppFactoryFixture factory) : base(factory) public TimeSeriesBaseControllerTest(WebAppFactoryFixture factory) : base(factory)
{ {
dbContext.CleanupDbSet<TEntity>(); dbContext.CleanupDbSet<TEntity>();
var scope = factory.Services.CreateScope(); Task.Run(async () =>
var persistenceClientFactory = scope.ServiceProvider {
.GetRequiredService<PersistenceClientFactory>(); client = await factory.GetAuthorizedHttpClient<ITimeSeriesClient<TDto>>(string.Empty);
}).Wait();
timeSeriesClient = persistenceClientFactory.GetClient<ITimeSeriesClient<TDto>>();
} }
public async Task InsertRangeSuccess(TDto dto) public async Task InsertRangeSuccess(TDto dto)
@ -30,7 +27,7 @@ public abstract class TimeSeriesBaseControllerTest<TEntity, TDto> : BaseIntegrat
var expected = dto.Adapt<TDto>(); var expected = dto.Adapt<TDto>();
//act //act
var response = await timeSeriesClient.InsertRange(new TDto[] { expected }); var response = await client.InsertRange(new TDto[] { expected });
//assert //assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@ -46,7 +43,7 @@ public abstract class TimeSeriesBaseControllerTest<TEntity, TDto> : BaseIntegrat
dbContext.SaveChanges(); dbContext.SaveChanges();
var response = await timeSeriesClient.Get(beginDate, endDate); var response = await client.Get(beginDate, endDate);
//assert //assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@ -68,7 +65,7 @@ public abstract class TimeSeriesBaseControllerTest<TEntity, TDto> : BaseIntegrat
dbContext.SaveChanges(); dbContext.SaveChanges();
var response = await timeSeriesClient.GetDatesRange(); var response = await client.GetDatesRange();
//assert //assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@ -98,7 +95,7 @@ public abstract class TimeSeriesBaseControllerTest<TEntity, TDto> : BaseIntegrat
dbContext.SaveChanges(); dbContext.SaveChanges();
var response = await timeSeriesClient.GetResampledData(entity.Date.AddMinutes(-1), differenceBetweenStartAndEndDays * 24 * 60 * 60 + 60, approxPointsCount); var response = await client.GetResampledData(entity.Date.AddMinutes(-1), differenceBetweenStartAndEndDays * 24 * 60 * 60 + 60, approxPointsCount);
//assert //assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(HttpStatusCode.OK, response.StatusCode);

View File

@ -0,0 +1,8 @@
using System.Text.Json.Serialization;
namespace Persistence.IntegrationTests;
public class JwtToken
{
[JsonPropertyName("access_token")]
public required string AccessToken { get; set; }
}

View File

@ -0,0 +1,27 @@
namespace Persistence.IntegrationTests;
/// <summary>
/// настройки credentials для пользователя в KeyCloak
/// </summary>
public class KeyCloakUser
{
/// <summary>
///
/// </summary>
public required string Username { get; set; }
/// <summary>
///
/// </summary>
public required string Password { get; set; }
/// <summary>
///
/// </summary>
public required string ClientId { get; set; }
/// <summary>
///
/// </summary>
public required string GrantType { get; set; }
}

View File

@ -25,7 +25,6 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Persistence.API\Persistence.API.csproj" /> <ProjectReference Include="..\Persistence.API\Persistence.API.csproj" />
<ProjectReference Include="..\Persistence.Client\Persistence.Client.csproj" />
<ProjectReference Include="..\Persistence.Database.Postgres\Persistence.Database.Postgres.csproj" /> <ProjectReference Include="..\Persistence.Database.Postgres\Persistence.Database.Postgres.csproj" />
</ItemGroup> </ItemGroup>

View File

@ -1,19 +0,0 @@
namespace Persistence.IntegrationTests
{
/// <summary>
/// Фабрика HTTP клиентов для интеграционных тестов
/// </summary>
public class TestHttpClientFactory : IHttpClientFactory
{
private readonly WebAppFactoryFixture factory;
public TestHttpClientFactory(WebAppFactoryFixture factory)
{
this.factory = factory;
}
public HttpClient CreateClient(string name)
{
return factory.CreateClient();
}
}
}

View File

@ -1,55 +1,67 @@
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Persistence.API; using Persistence.API;
using Persistence.Client;
using Persistence.Database.Model; using Persistence.Database.Model;
using Persistence.Database.Postgres; using Persistence.Database.Postgres;
using Refit;
using RestSharp; using RestSharp;
using System.Net.Http.Headers;
using System.Text.Json;
namespace Persistence.IntegrationTests; namespace Persistence.IntegrationTests;
public class WebAppFactoryFixture : WebApplicationFactory<Startup> public class WebAppFactoryFixture : WebApplicationFactory<Startup>
{ {
private string connectionString = string.Empty; 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<DbConnection>()!;
connectionString = dbConnection.GetConnectionString();
keycloakTestUser = configuration.GetSection("KeycloakTestUser").Get<KeyCloakUser>()!;
KeycloakGetTokenUrl = configuration.GetSection("KeycloakGetTokenUrl").Value!;
}
protected override void ConfigureWebHost(IWebHostBuilder builder) protected override void ConfigureWebHost(IWebHostBuilder builder)
{ {
builder.ConfigureAppConfiguration((hostingContext, config) => builder.ConfigureServices(services =>
{
config.AddJsonFile("appsettings.Tests.json");
var dbConnection = config.Build().GetSection("DbConnection").Get<DbConnection>()!;
connectionString = dbConnection.GetConnectionString();
});
builder.ConfigureServices(services =>
{ {
var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<PersistenceDbContext>)); var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<PersistenceDbContext>));
if (descriptor != null) if (descriptor != null)
services.Remove(descriptor); services.Remove(descriptor);
services.AddDbContext<PersistenceDbContext>(options =>
services.AddDbContext<PersistenceDbContext>(options =>
options.UseNpgsql(connectionString)); options.UseNpgsql(connectionString));
services.RemoveAll<IHttpClientFactory>(); var serviceProvider = services.BuildServiceProvider();
services.AddSingleton<IHttpClientFactory>(provider =>
{
return new TestHttpClientFactory(this);
});
services.AddSingleton<PersistenceClientFactory>();
var serviceProvider = services.BuildServiceProvider();
using var scope = serviceProvider.CreateScope(); using var scope = serviceProvider.CreateScope();
var scopedServices = scope.ServiceProvider; var scopedServices = scope.ServiceProvider;
var dbContext = scopedServices.GetRequiredService<PersistenceDbContext>(); var dbContext = scopedServices.GetRequiredService<PersistenceDbContext>();
dbContext.Database.EnsureCreatedAndMigrated(); dbContext.Database.EnsureCreatedAndMigrated();
dbContext.SaveChanges(); dbContext.SaveChanges();
}); });
} }
public override async ValueTask DisposeAsync() public override async ValueTask DisposeAsync()
@ -61,4 +73,57 @@ public class WebAppFactoryFixture : WebApplicationFactory<Startup>
await dbContext.Database.EnsureDeletedAsync(); await dbContext.Database.EnsureDeletedAsync();
} }
public T GetHttpClient<T>(string uriSuffix)
{
var httpClient = CreateClient();
if (string.IsNullOrEmpty(uriSuffix))
return RestService.For<T>(httpClient, RefitSettings);
if (httpClient.BaseAddress is not null)
httpClient.BaseAddress = new Uri(httpClient.BaseAddress, uriSuffix);
return RestService.For<T>(httpClient, RefitSettings);
}
public async Task<T> GetAuthorizedHttpClient<T>(string uriSuffix)
{
var httpClient = await GetAuthorizedHttpClient();
if (string.IsNullOrEmpty(uriSuffix))
return RestService.For<T>(httpClient, RefitSettings);
if (httpClient.BaseAddress is not null)
httpClient.BaseAddress = new Uri(httpClient.BaseAddress, uriSuffix);
return RestService.For<T>(httpClient, RefitSettings);
}
private async Task<HttpClient> GetAuthorizedHttpClient()
{
var httpClient = CreateClient();
var token = await GetTokenAsync();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
return httpClient;
}
private async Task<string> 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<JwtToken>(keyCloackResponse.Content)!;
return token.AccessToken;
}
return String.Empty;
}
} }

View File

@ -1,28 +0,0 @@
namespace Persistence.Repository.Data
{
/// <summary>
/// Модель для работы с уставкой
/// </summary>
public class SetpointDto
{
/// <summary>
/// Идентификатор уставки
/// </summary>
public int Id { get; set; }
/// <summary>
/// Значение уставки
/// </summary>
public required object Value { get; set; }
/// <summary>
/// Дата сохранения уставки
/// </summary>
public DateTimeOffset Edit { get; set; }
/// <summary>
/// Ключ пользователя
/// </summary>
public int IdUser { get; set; }
}
}

View File

@ -1,4 +1,4 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Persistence.Database.Model; using Persistence.Database.Model;
using Persistence.Repositories; using Persistence.Repositories;
using Persistence.Repository.Data; using Persistence.Repository.Data;
@ -15,8 +15,7 @@ public static class DependencyInjection
{ {
MapsterSetup(); MapsterSetup();
services.AddTransient<ITimeSeriesDataRepository<DataSaubDto>, TimeSeriesDataRepository<DataSaub, DataSaubDto>>(); services.AddTransient<ITimeSeriesDataRepository<DataSaubDto>, TimeSeriesDataCachedRepository<DataSaub, DataSaubDto>>();
services.AddTransient<ISetpointRepository, SetpointRepository>();
return services; return services;
} }

View File

@ -0,0 +1,68 @@
using Mapster;
using Microsoft.EntityFrameworkCore;
using Persistence.Database.Model;
using Persistence.Models;
using Persistence.Repositories;
namespace Persistence.Repository.Repositories;
public class ChangeLogRepository<TDto, TChangeLogDto> : IChangeLogRepository<TDto, TChangeLogDto>
where TDto : class, IChangeLogDto, new()
where TChangeLogDto : ChangeLogDto<TDto>
{
private DbContext db;
public ChangeLogRepository(DbContext db)
{
this.db = db;
}
public Task<int> Clear(int idUser, CancellationToken token)
{
throw new NotImplementedException();
}
public Task<int> ClearAndInsertRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token)
{
throw new NotImplementedException();
}
public Task<IEnumerable<TChangeLogDto>> GetChangeLogForDate(DateTimeOffset? updateFrom, CancellationToken token)
{
throw new NotImplementedException();
}
public Task<IEnumerable<TDto>> GetCurrent(DateTimeOffset moment, CancellationToken token)
{
throw new NotImplementedException();
}
public Task<IEnumerable<DateOnly>> GetDatesChange(CancellationToken token)
{
throw new NotImplementedException();
}
public Task<IEnumerable<TDto>> GetGtDate(DateTimeOffset dateBegin, CancellationToken token)
{
throw new NotImplementedException();
}
public Task<int> InsertRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token)
{
throw new NotImplementedException();
}
public Task<int> MarkAsDeleted(int idUser, IEnumerable<int> ids, CancellationToken token)
{
throw new NotImplementedException();
}
public Task<int> UpdateOrInsertRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token)
{
throw new NotImplementedException();
}
public Task<int> UpdateRange(int idUser, IEnumerable<TDto> dtos, CancellationToken token)
{
throw new NotImplementedException();
}
}

View File

@ -1,73 +0,0 @@
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<Setpoint> GetQueryReadOnly() => db.Set<Setpoint>();
public async Task<IEnumerable<SetpointValueDto>> GetCurrent(IEnumerable<Guid> 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<SetpointValueDto>());
return dtos;
}
public async Task<IEnumerable<SetpointValueDto>> GetHistory(IEnumerable<Guid> setpointKeys, DateTimeOffset historyMoment, CancellationToken token)
{
var query = GetQueryReadOnly();
var entities = await query
.Where(e => setpointKeys.Contains(e.Key))
.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<SetpointValueDto>());
return dtos;
}
public async Task<Dictionary<Guid, IEnumerable<SetpointLogDto>>> GetLog(IEnumerable<Guid> 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)
.ToDictionary(e => e.Key, v => v.Select(z => z.Adapt<SetpointLogDto>()));
return dtos;
}
public async Task Save(Guid setpointKey, object newValue, int idUser, CancellationToken token)
{
var entity = new Setpoint()
{
Key = setpointKey,
Value = newValue,
IdUser = idUser,
Created = DateTimeOffset.UtcNow
};
await db.Set<Setpoint>().AddAsync(entity, token);
await db.SaveChangesAsync(token);
}
}
}

View File

@ -13,9 +13,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Persistence.Database", "Per
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Persistence.IntegrationTests", "Persistence.IntegrationTests\Persistence.IntegrationTests.csproj", "{10752C25-3773-4081-A1F2-215A1D950126}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Persistence.IntegrationTests", "Persistence.IntegrationTests\Persistence.IntegrationTests.csproj", "{10752C25-3773-4081-A1F2-215A1D950126}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Persistence.Database.Postgres", "Persistence.Database.Postgres\Persistence.Database.Postgres.csproj", "{CC284D27-162D-490C-B6CF-74D666B7C5F3}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "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 EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -47,10 +45,6 @@ Global
{CC284D27-162D-490C-B6CF-74D666B7C5F3}.Debug|Any CPU.Build.0 = Debug|Any CPU {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.ActiveCfg = Release|Any CPU
{CC284D27-162D-490C-B6CF-74D666B7C5F3}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@ -1,9 +1,11 @@
namespace Persistence.Models; namespace Persistence.Models;
/// <summary> /// <summary>
/// Часть записи описывающая изменение /// Часть записи описывающая изменение
/// </summary> /// </summary>
public class ChangeLogDto<T> where T: class public class ChangeLogDto<T> : IChangeLogDto
where T: class
{ {
/// <summary> /// <summary>
/// Запись /// Запись
@ -11,32 +13,37 @@ public class ChangeLogDto<T> where T: class
public required T Item { get; set; } public required T Item { get; set; }
/// <summary> /// <summary>
/// Автор ///
/// </summary> /// </summary>
public UserDto? Author { get; set; } public int Id { get; set; }
/// <summary> /// <summary>
/// Автор ///
/// </summary> /// </summary>
public UserDto? Editor { get; set; } public int IdAuthor { get; set; }
/// <summary> /// <summary>
/// Дата создания записи ///
/// </summary>
public int? IdEditor { get; set; }
/// <summary>
///
/// </summary> /// </summary>
public DateTimeOffset Creation { get; set; } public DateTimeOffset Creation { get; set; }
/// <summary> /// <summary>
/// Дата устаревания (например, при удалении) ///
/// </summary> /// </summary>
public DateTimeOffset? Obsolete { get; set; } public DateTimeOffset? Obsolete { get; set; }
/// <summary> /// <summary>
/// Id состояния ///
/// </summary> /// </summary>
public int IdState { get; set; } public int? IdNext { get; set; }
/// <summary> /// <summary>
/// Id заменяемой записи ///
/// </summary> /// </summary>
public int? IdPrevious { get; set; } public required object Value { get; set; }
} }

View File

@ -1,12 +0,0 @@
namespace Persistence.Models.Configurations;
/// <summary>
/// Настройки credentials для авторизации
/// </summary>
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; }
}

View File

@ -1,18 +0,0 @@
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";
}
}

View File

@ -1,10 +0,0 @@
using System.Text.Json.Serialization;
namespace Persistence.Models.Configurations
{
public class JwtToken
{
[JsonPropertyName("access_token")]
public required string AccessToken { get; set; }
}
}

View File

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

View File

@ -0,0 +1,42 @@
namespace Persistence.Models;
/// <summary>
/// Часть записи описывающая изменение
/// </summary>
public interface IChangeLogDto
{
///// <summary>
///// Ид записи
///// </summary>
//public int Id { get; set; }
///// <summary>
///// Автор изменения
///// </summary>
//public int IdAuthor { get; set; }
///// <summary>
///// Редактор
///// </summary>
//public int? IdEditor { get; set; }
///// <summary>
///// Дата создания записи
///// </summary>
//public DateTimeOffset Creation { get; set; }
///// <summary>
///// Дата устаревания (например при удалении)
///// </summary>
//public DateTimeOffset? Obsolete { get; set; }
///// <summary>
///// Id заменяющей записи
///// </summary>
//public int? IdNext { get; set; }
///// <summary>
///// Значение
///// </summary>
//public object Value { get; set; }
}

View File

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Persistence.Models;
public class ProcessMapRotorDto : IChangeLogDto
{
public string Caption { get; set; }
}

View File

@ -1,4 +1,4 @@
namespace Persistence.Models; namespace Persistence.Models;
/// <summary> /// <summary>
/// Модель для описания лога уставки /// Модель для описания лога уставки
@ -8,8 +8,8 @@ public class SetpointLogDto : SetpointValueDto
/// <summary> /// <summary>
/// Дата сохранения уставки /// Дата сохранения уставки
/// </summary> /// </summary>
public DateTimeOffset Created { get; set; } public DateTimeOffset DateEdit { get; set; }
/// <summary> /// <summary>
/// Ключ пользователя /// Ключ пользователя
/// </summary> /// </summary>

View File

@ -1,18 +1,18 @@
namespace Persistence.Models; namespace Persistence.Models;
/// <summary> /// <summary>
/// Модель для хранения значения уставки /// Модель для хранения значения уставки
/// </summary> /// </summary>
public class SetpointValueDto public class SetpointValueDto
{ {
/// Идентификатор уставки
/// <summary> /// <summary>
/// Идентификатор уставки
/// </summary> /// </summary>
public Guid Key { get; set; } public int Id { get; set; }
/// <summary> /// <summary>
/// Значение уставки /// Значение уставки
/// </summary> /// </summary>
public required object Value { get; set; } public object Value { get; set; }
} }

View File

@ -9,7 +9,6 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.10" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.10" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.2.1" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

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

View File

@ -1,4 +1,4 @@
using Persistence.Models; using Persistence.Models;
namespace Persistence.Repositories; namespace Persistence.Repositories;
@ -7,30 +7,23 @@ namespace Persistence.Repositories;
/// </summary> /// </summary>
public interface ISetpointRepository public interface ISetpointRepository
{ {
/// <summary>
/// Получить значения уставок по набору ключей
/// </summary>
/// <param name="setpointKeys"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<SetpointValueDto>> GetCurrent(IEnumerable<Guid> setpointKeys, CancellationToken token);
/// <summary> /// <summary>
/// Получить значения уставок за определенный момент времени /// Получить значения уставок за определенный момент времени
/// </summary> /// </summary>
/// <param name="setpointKeys"></param> /// <param name="setpoitKeys"></param>
/// <param name="historyMoment">дата, на которую получаем данные</param> /// <param name="historyMoment">дата, на которую получаем данные</param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<IEnumerable<SetpointValueDto>> GetHistory(IEnumerable<Guid> setpointKeys, DateTimeOffset historyMoment, CancellationToken token); Task<IEnumerable<SetpointValueDto>> GetHistory(IEnumerable<Guid> setpoitKeys, DateTimeOffset historyMoment, CancellationToken token);
/// <summary> /// <summary>
/// Получить историю изменений значений уставок /// Получить историю изменений значений уставок
/// </summary> /// </summary>
/// <param name="setpointKeys"></param> /// <param name="setpoitKeys"></param>
/// <param name="token"></param> /// <param name="token"></param>
/// <returns></returns> /// <returns></returns>
Task<Dictionary<Guid, IEnumerable<SetpointLogDto>>> GetLog(IEnumerable<Guid> setpointKeys, CancellationToken token); Task<Dictionary<Guid, IEnumerable<SetpointLogDto>>> GetLog(IEnumerable<Guid> setpoitKeys, CancellationToken token);
/// <summary> /// <summary>
/// Метод сохранения уставки /// Метод сохранения уставки
@ -42,5 +35,5 @@ public interface ISetpointRepository
/// <returns></returns> /// <returns></returns>
/// to do /// to do
/// id User учесть в соответствующем методе репозитория /// id User учесть в соответствующем методе репозитория
Task Save(Guid setpointKey, object newValue, int idUser, CancellationToken token); Task<int> Save(Guid setpointKey, int idUser, object newValue, CancellationToken token);
} }