diff --git a/Persistence.API/Controllers/DataSaubController.cs b/Persistence.API/Controllers/DataSaubController.cs index 60b4e48..63069a9 100644 --- a/Persistence.API/Controllers/DataSaubController.cs +++ b/Persistence.API/Controllers/DataSaubController.cs @@ -1,9 +1,11 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Persistence.Repositories; using Persistence.Repository.Data; namespace Persistence.API.Controllers; [ApiController] +[Authorize] [Route("api/[controller]")] public class DataSaubController : TimeSeriesController { diff --git a/Persistence.API/Controllers/TimeSeriesController.cs b/Persistence.API/Controllers/TimeSeriesController.cs index e094cc4..e6c74ef 100644 --- a/Persistence.API/Controllers/TimeSeriesController.cs +++ b/Persistence.API/Controllers/TimeSeriesController.cs @@ -1,9 +1,11 @@ +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 TimeSeriesController : ControllerBase, ITimeSeriesDataApi where TDto : class, ITimeSeriesAbstractDto, new() @@ -21,6 +23,7 @@ public class TimeSeriesController : ControllerBase, ITimeSeriesDataApi + { + c.MapType(() => new OpenApiSchema { Type = "string", Example = new OpenApiString("0.00:00:00") }); + c.MapType(() => new OpenApiSchema { Type = "string", Format = "date" }); + c.MapType(() => new OpenApiSchema + { + AnyOf = new OpenApiSchema[] + { + new OpenApiSchema {Type = "string", Format = "string" }, + new OpenApiSchema {Type = "number", Format = "int32" }, + new OpenApiSchema {Type = "number", Format = "float" }, + } + }); + + c.CustomOperationIds(e => + { + return $"{e.ActionDescriptor.RouteValues["action"]}"; + }); + + 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 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"], + }; + }); + } +} diff --git a/Persistence.API/Extensions.cs b/Persistence.API/Extensions.cs new file mode 100644 index 0000000..601a2f4 --- /dev/null +++ b/Persistence.API/Extensions.cs @@ -0,0 +1,26 @@ +using System.ComponentModel; +using System.Security.Claims; + +namespace Persistence.API; + +public static class Extensions +{ + public static T GetUserId(this ClaimsPrincipal principal) + { + if (principal == null) + throw new ArgumentNullException(nameof(principal)); + + var loggedInUserId = principal.FindFirstValue(ClaimTypes.NameIdentifier); + + if (String.IsNullOrEmpty(loggedInUserId)) + throw new ArgumentNullException(nameof(loggedInUserId)); + + var result = TypeDescriptor.GetConverter(typeof(T)).ConvertFromInvariantString(loggedInUserId); + + if (result is null) + throw new ArgumentNullException(nameof(result)); + + return (T)result; + + } +} diff --git a/Persistence.API/Persistence.API.csproj b/Persistence.API/Persistence.API.csproj index 40bed8a..2b8cb73 100644 --- a/Persistence.API/Persistence.API.csproj +++ b/Persistence.API/Persistence.API.csproj @@ -8,6 +8,7 @@ + diff --git a/Persistence.API/Startup.cs b/Persistence.API/Startup.cs index 1afaa18..32466c8 100644 --- a/Persistence.API/Startup.cs +++ b/Persistence.API/Startup.cs @@ -19,9 +19,10 @@ public class Startup services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle services.AddEndpointsApiExplorer(); - services.AddSwaggerGen(); - services.AddPersistenceDbContext(Configuration); + services.AddSwagger(Configuration); services.AddInfrastructure(); + services.AddPersistenceDbContext(Configuration); + services.AddJWTAuthentication(Configuration); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) @@ -32,7 +33,8 @@ public class Startup app.UseRouting(); - //app.UseAuthorization(); + app.UseAuthentication(); + app.UseAuthorization(); app.UseEndpoints(endpoints => { diff --git a/Persistence.API/appsettings.json b/Persistence.API/appsettings.json index 14eabf9..2e0033b 100644 --- a/Persistence.API/appsettings.json +++ b/Persistence.API/appsettings.json @@ -6,7 +6,13 @@ } }, "ConnectionStrings": { - "DefaultConnection": "Host=localhost;Database=persistence;Username=postgres;Password=q;Persist Security Info=True", + "DefaultConnection": "Host=localhost;Database=persistence;Username=postgres;Password=q;Persist Security Info=True" }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "Authentication": { + "MetadataAddress": "http://localhost:8080/realms/TestRealm/.well-known/openid-configuration", + "Audience": "account", + "ValidIssuer": "http://localhost:8080/realms/TestRealm", + "AuthorizationUrl": "http://localhost:8080/realms/TestRealm/protocol/openid-connect/auth" + } } diff --git a/Persistence.Repository/CyclicArray.cs b/Persistence.Repository/CyclicArray.cs new file mode 100644 index 0000000..fa6d074 --- /dev/null +++ b/Persistence.Repository/CyclicArray.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Persistence.Repository; +/// +/// Цикличный массив +/// +/// +public class CyclicArray : IEnumerable +{ + readonly T[] array; + int used, current = -1; + + /// + /// constructor + /// + /// + public CyclicArray(int capacity) + { + array = new T[capacity]; + } + + /// + /// Количество элементов в массиве + /// + public int Count => used; + + /// + /// Добавить новый элемент
+ /// Если capacity достигнуто, то вытеснит самый первый элемент + ///
+ /// + public void Add(T item) + { + current = (++current) % array.Length; + array[current] = item; + if (used < array.Length) + used++; + UpdatedInvoke(current, item); + } + + /// + /// Добавить новые элементы.
+ /// Если capacity достигнуто, то вытеснит самые первые элементы.
+ /// Не вызывает Updated! + ///
+ /// + public void AddRange(IEnumerable items) + { + var capacity = array.Length; + var newItems = items.TakeLast(capacity).ToArray(); + if (newItems.Length == capacity) + { + Array.Copy(newItems, array, capacity); + current = capacity - 1; + } + else + { + current = (++current) % capacity; + var countToEndOfArray = capacity - current; + if (newItems.Length <= countToEndOfArray) + { + Array.Copy(newItems, 0, array, current, newItems.Length); + current += newItems.Length - 1; + } + else + { + var firstStepLength = countToEndOfArray; + Array.Copy(newItems, 0, array, current, firstStepLength); + var secondStepCount = newItems.Length - firstStepLength; + Array.Copy(newItems, firstStepLength, array, 0, secondStepCount); + current = secondStepCount - 1; + } + } + + if (used < capacity) + { + used += newItems.Length; + used = used > capacity ? capacity : used; + } + } + + /// + /// Индекс + /// + /// + /// + public T this[int index] + { + get + { + if (used == 0) + throw new IndexOutOfRangeException(); + + var i = (current + 1 + index) % used; + return array[i]; + } + set + { + var devider = used > 0 ? used : array.Length; + var i = (current + 1 + index) % devider; + array[i] = value; + UpdatedInvoke(current, value); + } + } + + /// + /// событие на изменение элемента в массиве + /// + public event EventHandler<(int index, T value)>? Updated; + private void UpdatedInvoke(int index, T value) + { + Updated?.Invoke(this, (index, value)); + } + + /// + /// Агрегирование значения по всему массиву + /// + /// + /// + /// + /// + public Tout Aggregate(Func func, Tout startValue) + { + Tout result = startValue; + for (int i = 0; i < used; i++) + result = func(this[i], result); + return result; + } + + /// + public IEnumerator GetEnumerator() + => new CyclycListEnumerator(array, current, used); + + /// + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + class CyclycListEnumerator : IEnumerator + { + private readonly Te[] array; + private readonly int used; + private readonly int first; + private int current = -1; + + public CyclycListEnumerator(Te[] array, int first, int used) + { + this.array = new Te[array.Length]; + array.CopyTo(this.array, 0); + this.used = used; + this.first = first; + } + + public Te Current + { + get + { + if (IsCurrentOk()) + { + var i = (current + first + 1) % used; + return array[i]; + } + else + return default!; + } + } + + object? IEnumerator.Current => Current; + + public void Dispose() {; } + + private bool IsCurrentOk() => current >= 0 && current < used; + + public bool MoveNext() + { + if (current < used) + current++; + return IsCurrentOk(); + } + + public void Reset() + { + current = -1; + } + } + + /// + /// Очистить весь массив + /// + public void Clear() + { + used = 0; + current = -1; + } +} diff --git a/Persistence.Repository/DependencyInjection.cs b/Persistence.Repository/DependencyInjection.cs index 6f0c382..27063cb 100644 --- a/Persistence.Repository/DependencyInjection.cs +++ b/Persistence.Repository/DependencyInjection.cs @@ -1,11 +1,8 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Persistence.Repositories; using Persistence.Database.Model; +using Persistence.Repositories; using Persistence.Repository.Data; using Persistence.Repository.Repositories; -using Persistence.Database; namespace Persistence.Repository; public static class DependencyInjection diff --git a/Persistence.Repository/Repositories/TimeSeriesDataCachedRepository.cs b/Persistence.Repository/Repositories/TimeSeriesDataCachedRepository.cs new file mode 100644 index 0000000..4a553ac --- /dev/null +++ b/Persistence.Repository/Repositories/TimeSeriesDataCachedRepository.cs @@ -0,0 +1,68 @@ +using Mapster; +using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json.Linq; +using Persistence.Database.Model; +using Persistence.Models; + +namespace Persistence.Repository.Repositories; + +public class TimeSeriesDataCachedRepository : TimeSeriesDataRepository + where TEntity : class, ITimestampedData, new() + where TDto : class, ITimeSeriesAbstractDto, new() +{ + public static TDto FirstByDate { get; set; } = default!; + public static CyclicArray LastData { get; set; } = null!; + + private const int CacheItemsCount = 3600; + + public TimeSeriesDataCachedRepository(DbContext db) : base(db) + { + Task.Run(async () => + { + LastData = new CyclicArray(CacheItemsCount); + + var firstDateItem = await base.GetFirstAsync(CancellationToken.None); + if (firstDateItem == null) + { + return; + } + + FirstByDate = firstDateItem; + + var dtos = await base.GetLastAsync(CacheItemsCount, CancellationToken.None); + dtos = dtos.OrderBy(d => d.Date); + LastData.AddRange(dtos); + }).Wait(); + } + + public override async Task> GetAsync(DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token) + { + + if (LastData.Count() == 0 || LastData[0].Date > dateBegin) + { + var dtos = await base.GetAsync(dateBegin, dateEnd, token); + return dtos; + } + + var items = LastData + .Where(i => i.Date >= dateBegin && i.Date <= dateEnd); + + return items; + } + + public override async Task InsertRange(IEnumerable dtos, CancellationToken token) + { + var result = await base.InsertRange(dtos, token); + if (result > 0) + { + + dtos = dtos.OrderBy(x => x.Date); + + FirstByDate = dtos.First(); + LastData.AddRange(dtos); + } + + return result; + } +} + diff --git a/Persistence.Repository/Repositories/TimeSeriesDataRepository.cs b/Persistence.Repository/Repositories/TimeSeriesDataRepository.cs index df8c520..9b339a5 100644 --- a/Persistence.Repository/Repositories/TimeSeriesDataRepository.cs +++ b/Persistence.Repository/Repositories/TimeSeriesDataRepository.cs @@ -18,7 +18,7 @@ public class TimeSeriesDataRepository : ITimeSeriesDataRepository protected virtual IQueryable GetQueryReadOnly() => this.db.Set(); - public async Task> GetAsync(DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token) + public virtual async Task> GetAsync(DateTimeOffset dateBegin, DateTimeOffset dateEnd, CancellationToken token) { var query = GetQueryReadOnly(); var entities = await query.ToArrayAsync(token); @@ -27,7 +27,7 @@ public class TimeSeriesDataRepository : ITimeSeriesDataRepository return dtos; } - public async Task GetDatesRangeAsync(CancellationToken token) + public virtual async Task GetDatesRangeAsync(CancellationToken token) { var query = GetQueryReadOnly(); var minDate = await query.MinAsync(o => o.Date, token); @@ -40,7 +40,7 @@ public class TimeSeriesDataRepository : ITimeSeriesDataRepository }; } - public async Task> GetGtDate(DateTimeOffset date, CancellationToken token) + public virtual async Task> GetGtDate(DateTimeOffset date, CancellationToken token) { var query = this.db.Set().Where(e => e.Date > date); var entities = await query.ToArrayAsync(token); @@ -50,7 +50,7 @@ public class TimeSeriesDataRepository : ITimeSeriesDataRepository return dtos; } - public async Task InsertRange(IEnumerable dtos, CancellationToken token) + public virtual async Task InsertRange(IEnumerable dtos, CancellationToken token) { var entities = dtos.Select(d => d.Adapt()); @@ -59,4 +59,30 @@ public class TimeSeriesDataRepository : ITimeSeriesDataRepository return result; } + + public async Task> GetLastAsync(int takeCount, CancellationToken token) + { + var query = GetQueryReadOnly() + .OrderByDescending(e => e.Date) + .Take(takeCount); + + var entities = await query.ToArrayAsync(token); + var dtos = entities.Select(e => e.Adapt()); + + return dtos; + } + + public async Task GetFirstAsync(CancellationToken token) + { + var query = GetQueryReadOnly() + .OrderBy(e => e.Date); + + var entity = await query.FirstOrDefaultAsync(token); + + if(entity == null) + return null; + + var dto = entity.Adapt(); + return dto; + } } diff --git a/Persistence/Repositories/ITimeSeriesDataRepository.cs b/Persistence/Repositories/ITimeSeriesDataRepository.cs index 16aae46..f3e49eb 100644 --- a/Persistence/Repositories/ITimeSeriesDataRepository.cs +++ b/Persistence/Repositories/ITimeSeriesDataRepository.cs @@ -31,4 +31,19 @@ public interface ITimeSeriesDataRepository : ISyncRepository /// /// Task InsertRange(IEnumerable dtos, CancellationToken token); + + /// + /// Получение списка последних записей + /// + /// количество записей + /// + /// + Task> GetLastAsync(int takeCount, CancellationToken token); + + /// + /// Получение первой записи + /// + /// + /// + Task GetFirstAsync(CancellationToken token); }