Merge branch 'dev' into feature/7887519

This commit is contained in:
ai.astrakhantsev 2022-11-18 14:30:34 +05:00
commit 49cc9c2342
52 changed files with 787 additions and 656 deletions

View File

@ -18,10 +18,6 @@
<EditorConfigFiles Remove="D:\Source\AsbCloudApp\Services\.editorconfig" />
</ItemGroup>
<ItemGroup>
<None Include="D:\Source\AsbCloudApp\Services\.editorconfig" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AsbCloudDb\AsbCloudDb.csproj" />
</ItemGroup>

198
AsbCloudApp/CyclycArray.cs Normal file
View File

@ -0,0 +1,198 @@
#nullable enable
using System.Linq;
namespace System.Collections.Generic
{
/// <summary>
/// Цикличный массив
/// </summary>
/// <typeparam name="T"></typeparam>
public class CyclycArray<T> : IEnumerable<T>
{
readonly T[] array;
int used, current = -1;
/// <summary>
/// constructor
/// </summary>
/// <param name="capacity"></param>
public CyclycArray(int capacity)
{
array = new T[capacity];
}
/// <summary>
/// Количество элементов в массиве
/// </summary>
public int Count => used;
/// <summary>
/// Добавить новый элемент<br/>
/// Если capacity достигнуто, то вытеснит самый первый элемент
/// </summary>
/// <param name="item"></param>
public void Add(T item)
{
current = (++current) % array.Length;
array[current] = item;
if (used < array.Length)
used++;
UpdatedInvoke(current, item);
}
/// <summary>
/// Добавить новые элементы.<br/>
/// Если capacity достигнуто, то вытеснит самые первые элементы.<br/>
/// Не вызывает Updated!
/// </summary>
/// <param name="items"></param>
public void AddRange(IEnumerable<T> 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;
}
}
/// <summary>
/// Индекс
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
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);
}
}
/// <summary>
/// событие на изменение элемента в массиве
/// </summary>
public event EventHandler<(int index, T value)>? Updated;
private void UpdatedInvoke(int index, T value)
{
Updated?.Invoke(this, (index, value));
}
/// <summary>
/// Агрегирование значения по всему массиву
/// </summary>
/// <typeparam name="Tout"></typeparam>
/// <param name="func"></param>
/// <param name="startValue"></param>
/// <returns></returns>
public Tout Aggregate<Tout>(Func<T, Tout, Tout> func, Tout startValue)
{
Tout result = startValue;
for (int i = 0; i < used; i++)
result = func(this[i], result);
return result;
}
/// <inheritdoc/>
public IEnumerator<T> GetEnumerator()
=> new CyclycListEnumerator<T>(array, current, used);
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
class CyclycListEnumerator<Te> : IEnumerator<Te>
{
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;
}
}
/// <summary>
/// Очистить весь массив
/// </summary>
public void Clear()
{
used = 0;
current = -1;
}
}
}
#nullable disable

View File

@ -10,7 +10,7 @@ namespace AsbCloudApp.Data.Subsystems
/// <summary>
/// Активная скважина
/// </summary>
public WellDto Well { get; set; }
public WellDto Well { get; set; } = null!;
/// <summary>
/// Наработки подсистемы АКБ
/// </summary>

View File

@ -51,7 +51,6 @@ namespace AsbCloudApp.Repositories
/// <summary>
/// Вывод списка всех файлов из базы, для которых нет файла на диске
/// </summary>
/// <param name="idWell"></param>
/// <param name="files"></param>
/// <returns></returns>
IEnumerable<FileInfoDto> GetListFilesNotDisc(IEnumerable<FileInfoDto> files);

View File

@ -165,15 +165,6 @@ namespace AsbCloudApp.Services
public async Task<IEnumerable<FileInfoDto>> GetInfoByIdsAsync(IEnumerable<int> idsFile, CancellationToken token)
{
var result = await fileRepository.GetInfoByIdsAsync(idsFile, token).ConfigureAwait(false);
foreach (var entity in result)
{
var ext = Path.GetExtension(entity.Name);
var relativePath = GetUrl(entity.IdWell, entity.IdCategory, entity.Id, ext);
var fullPath = Path.GetFullPath(relativePath);
}
return result;
}

View File

@ -5,6 +5,7 @@ using System.Threading.Tasks;
namespace AsbCloudApp.Services
{
#nullable enable
/// <summary>
/// Сервис авторизации
/// </summary>
@ -33,13 +34,14 @@ namespace AsbCloudApp.Services
/// <param name="password"></param>
/// <param name="token">токен отмены задачи</param>
/// <returns></returns>
Task<UserTokenDto> LoginAsync(string login,
Task<UserTokenDto?> LoginAsync(string login,
string password, CancellationToken token = default);
/// <summary>
/// Обновление токена авторизации
/// </summary>
/// <param name="user"></param>
/// <param name="identity"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<UserTokenDto?> RefreshAsync(ClaimsPrincipal identity,
CancellationToken token);
@ -51,4 +53,5 @@ namespace AsbCloudApp.Services
/// <returns></returns>
int Register(UserRegistrationDto userDto);
}
#nullable disable
}

View File

@ -10,6 +10,12 @@ namespace AsbCloudApp.Services
/// </summary>
public interface IFileCategoryService
{
/// <summary>
/// Получить категории файлов
/// </summary>
/// <param name="id"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<FileCategoryDto> GetOrDefaultAsync(int id, CancellationToken token);
/// <summary>

View File

@ -3,7 +3,7 @@ using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
#nullable enable
namespace AsbCloudApp.Services
{
/// <summary>
@ -22,7 +22,7 @@ namespace AsbCloudApp.Services
/// <param name="approxPointsCount">кол-во элементов до которых эти данные прореживаются</param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<TDto>> GetAsync(int idWell,
Task<IEnumerable<TDto>?> GetOrDefaultAsync(int idWell,
DateTime dateBegin = default, double intervalSec = 600d,
int approxPointsCount = 1024, CancellationToken token = default);
@ -36,3 +36,4 @@ namespace AsbCloudApp.Services
Task<int> UpdateDataAsync(string uid, IEnumerable<TDto> dtos, CancellationToken token = default);
}
}
#nullable disable

View File

@ -55,7 +55,7 @@ namespace AsbCloudApp.Services
/// </summary>
/// <param name="idWell"></param>
/// <returns></returns>
int? GetIdTelemetryByIdWell(int idWell);
int? GetOrDefaultIdTelemetryByIdWell(int idWell);
/// <summary>
/// получить диапазон дат за которые есть данные

View File

@ -6,8 +6,19 @@ using System.Threading.Tasks;
namespace AsbCloudApp.Services.Subsystems
{
#nullable enable
// TODO: move this to repositories
/// <summary>
/// репозиторий получения подсистем
/// </summary>
public interface ISubsystemService
{
/// <summary>
/// получение списка подсистем. Если скважина указана, то получим только использованные в скважине подсистемы.
/// </summary>
/// <param name="idWell"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<IEnumerable<SubsystemDto>?> GetSubsystemAsync(int? idWell, CancellationToken token);
}
#nullable disable

View File

@ -61,14 +61,6 @@ namespace AsbCloudDb.Model
public DbSet<WITS.Record60> Record60 => Set<WITS.Record60>();
public DbSet<WITS.Record61> Record61 => Set<WITS.Record61>();
private System.Text.Json.JsonSerializerOptions jsonSerializerOptions = new()
{
AllowTrailingCommas = true,
WriteIndented = true,
NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString |
System.Text.Json.Serialization.JsonNumberHandling.AllowNamedFloatingPointLiterals,
};
public AsbCloudDbContext() : base()
{
}
@ -369,10 +361,5 @@ namespace AsbCloudDb.Model
var sql = $"REFRESH MATERIALIZED VIEW {materializedViewName};";
return Database.ExecuteSqlRawAsync(sql, token);
}
public Task<int> RefreshMaterializedViewAsync<TEntity>(string? mwName = null, CancellationToken token = default) where TEntity : class
{
throw new System.NotImplementedException();
}
}
}

View File

@ -5,58 +5,57 @@
/// <summary>
/// КНБК описание
/// </summary>
public string BHADescription { get; set; }
public string BHADescription { get; set; } = string.Empty;
/// <summary>
/// Бурение с наращиваниями в инт. 2195-2763м. Время начала
/// </summary>
public string ExtensionDrillingOneBegin { get; set; }
public string ExtensionDrillingOneBegin { get; set; } = string.Empty;
/// <summary>
/// Бурение с наращиваниями в инт. 2195-2763м. Время окончания
/// </summary>
public string ExtensionDrillingOneFinish { get; set; }
public string ExtensionDrillingOneFinish { get; set; } = string.Empty;
/// <summary>
/// Промывка. Время начала
/// </summary>
public string SluiceBegin { get; set; }
public string SluiceBegin { get; set; } = string.Empty;
/// <summary>
/// Промывка. Время окончания
/// </summary>
public string SluiceFinish { get; set; }
public string SluiceFinish { get; set; } = string.Empty;
/// <summary>
/// Подьем КНБК. Время начала
/// </summary>
public string ClimbBegin { get; set; }
public string ClimbBegin { get; set; } = string.Empty;
/// <summary>
/// Подьем КНБК. Время окончания
/// </summary>
public string ClimbFinish { get; set; }
public string ClimbFinish { get; set; } = string.Empty;
/// <summary>
/// Спуск КНБК. Время начала
/// </summary>
public string DescentBegin { get; set; }
public string DescentBegin { get; set; } = string.Empty;
/// <summary>
/// Спуск КНБК. Время окончания
/// </summary>
public string DescentFinish { get; set; }
public string DescentFinish { get; set; } = string.Empty;
/// <summary>
/// Бурение с наращиваниями в инт. 2763-2850м. Время начала
/// </summary>
public string ExtensionDrillingTwoBegin { get; set; }
public string ExtensionDrillingTwoBegin { get; set; } = string.Empty;
/// <summary>
/// Бурение с наращиваниями в инт. 2763-2850м. Время окончания
/// </summary>
public string ExtensionDrillingTwoFinish { get; set; }
public string ExtensionDrillingTwoFinish { get; set; } = string.Empty;
}
}

View File

@ -6,22 +6,22 @@ namespace AsbCloudDb.Model.DailyReport
/// <summary>
/// название скважины
/// </summary>
public string WellName { get; set; }
public string WellName { get; set; } = string.Empty;
/// <summary>
/// название куста
/// </summary>
public string ClusterName { get; set; }
public string ClusterName { get; set; } = string.Empty;
/// <summary>
/// заказчик
/// </summary>
public string Customer { get; set; }
public string Customer { get; set; } = string.Empty;
/// <summary>
/// подрядчик
/// </summary>
public string Contractor { get; set; }
public string Contractor { get; set; } = string.Empty;
/// <summary>
/// дата рапорта
@ -61,12 +61,12 @@ namespace AsbCloudDb.Model.DailyReport
/// <summary>
/// ФИО бурильщиков
/// </summary>
public string FirstDriller { get; set; }
public string FirstDriller { get; set; } = string.Empty;
/// <summary>
/// ФИО бурильщиков
/// </summary>
public string SecondDriller { get; set; }
public string SecondDriller { get; set; } = string.Empty;
/// <summary>
/// Время работы АПД

View File

@ -72,32 +72,32 @@ namespace AsbCloudDb.Model.DailyReport
/// <summary>
/// указываются все причины, которые влияют на снижение МСП.
/// </summary>
public string DeclinesReasonsROP { get; set; }
public string DeclinesReasonsROP { get; set; } = string.Empty;
/// <summary>
/// Увеличение мех скорости за секцию %
/// </summary>
public string IncreaseSpeedSection { get; set; }
public string IncreaseSpeedSection { get; set; } = string.Empty;
/// <summary>
/// Увеличение мех скорости за сутки %
/// </summary>
public string IncreaseSpeedDay { get; set; }
public string IncreaseSpeedDay { get; set; } = string.Empty;
/// <summary>
/// Сокращение времени бурения за секцию, ч
/// </summary>
public string ReductionTimeDrilling { get; set; }
public string ReductionTimeDrilling { get; set; } = string.Empty;
/// <summary>
/// Ротор/Слайд %
/// </summary>
public string RotorSlidePercent { get; set; }
public string RotorSlidePercent { get; set; } = string.Empty;
/// <summary>
/// МСП
/// </summary>
public string MspSection { get; set; }
public string MspSection { get; set; } = string.Empty;
}
}

View File

@ -5,12 +5,12 @@
/// <summary>
/// ФИО Мастера буровой
/// </summary>
public string DrillingMaster { get; set; }
public string DrillingMaster { get; set; } = string.Empty;
/// <summary>
/// ФИО супервайзера
/// </summary>
public string Supervisor { get; set; }
public string Supervisor { get; set; } = string.Empty;
}
}

View File

@ -5,92 +5,92 @@
/// <summary>
/// Бурение
/// </summary>
public string Drilling { get; set; }
public string Drilling { get; set; } = string.Empty;
/// <summary>
/// Промывка
/// </summary>
public string Flushing { get; set; }
public string Flushing { get; set; } = string.Empty;
/// <summary>
/// Наращивание
/// </summary>
public string Building { get; set; }
public string Building { get; set; } = string.Empty;
/// <summary>
/// Проработка
/// </summary>
public string Elaboration { get; set; }
public string Elaboration { get; set; } = string.Empty;
/// <summary>
/// Расширка
/// </summary>
public string Extension { get; set; }
public string Extension { get; set; } = string.Empty;
/// <summary>
/// Ремонт
/// </summary>
public string Repair { get; set; }
public string Repair { get; set; } = string.Empty;
/// <summary>
/// КНБК
/// </summary>
public string Knbk { get; set; }
public string Knbk { get; set; } = string.Empty;
/// <summary>
/// СПО
/// </summary>
public string Spo { get; set; }
public string Spo { get; set; } = string.Empty;
/// <summary>
/// ПЗР
/// </summary>
public string Pzr { get; set; }
public string Pzr { get; set; } = string.Empty;
/// <summary>
/// ПВО
/// </summary>
public string Pvo { get; set; }
public string Pvo { get; set; } = string.Empty;
/// <summary>
/// ПГР
/// </summary>
public string Pgr { get; set; }
public string Pgr { get; set; } = string.Empty;
/// <summary>
/// ГИС
/// </summary>
public string Gis { get; set; }
public string Gis { get; set; } = string.Empty;
/// <summary>
/// ОЗЦ
/// </summary>
public string Ozc { get; set; }
public string Ozc { get; set; } = string.Empty;
/// <summary>
/// Тех. работы
/// </summary>
public string EngineeringWorks { get; set; }
public string EngineeringWorks { get; set; } = string.Empty;
/// <summary>
/// Снятие замера
/// </summary>
public string TakingMeasure { get; set; }
public string TakingMeasure { get; set; } = string.Empty;
/// <summary>
/// Цементирование
/// </summary>
public string Cementing { get; set; }
public string Cementing { get; set; } = string.Empty;
/// <summary>
/// Простой
/// </summary>
public string Simple { get; set; }
public string Simple { get; set; } = string.Empty;
/// <summary>
/// НПВ
/// </summary>
public string Npv { get; set; }
public string Npv { get; set; } = string.Empty;
}
}

View File

@ -16,6 +16,7 @@ using AsbCloudInfrastructure.Services.SAUB;
using AsbCloudInfrastructure.Services.Subsystems;
using AsbCloudInfrastructure.Services.WellOperationService;
using AsbCloudInfrastructure.Validators;
using DocumentFormat.OpenXml.Spreadsheet;
using FluentValidation.AspNetCore;
using Mapster;
using Microsoft.EntityFrameworkCore;
@ -90,7 +91,8 @@ namespace AsbCloudInfrastructure
services.AddDbContext<AsbCloudDbContext>(options =>
options.UseNpgsql(configuration.GetConnectionString(connectionStringName)));
services.AddFluentValidation();
// TODO: переместить FluentValidation в описание моделей
services.AddFluentValidationClientsideAdapters();
services.AddScoped<IAsbCloudDbContext>(provider => provider.GetService<AsbCloudDbContext>());
services.AddScoped<IEmailService, EmailService>();
@ -101,6 +103,8 @@ namespace AsbCloudInfrastructure
services.AddSingleton(new WitsInfoService());
services.AddSingleton(new CacheDb());
services.AddSingleton(new InstantDataRepository());
services.AddSingleton(provider=> TelemetryDataCache<TelemetryDataSaubDto>.GetInstance<TelemetryDataSaub>(configuration));
services.AddSingleton(provider=> TelemetryDataCache<TelemetryDataSpinDto>.GetInstance<TelemetryDataSpin>(configuration));
services.AddSingleton<ITelemetryTracker, TelemetryTracker>();
services.AddSingleton<IRequerstTrackerService, RequestTrackerService>();
services.AddSingleton<IBackgroundWorkerService, BackgroundWorkerService>();

View File

@ -1,439 +0,0 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace AsbCloudInfrastructure.EfCache
{
#nullable enable
/// <summary>
/// Кеширование запросов EF.
/// Кеш не отслеживается ChangeTracker.
/// </summary>
public static class EfCacheDictionaryExtensions
{
private static readonly Dictionary<string, CacheItem> caches = new(16);
private static readonly TimeSpan semaphoreTimeout = TimeSpan.FromSeconds(25);
private static readonly SemaphoreSlim semaphore = new(1);
private static readonly TimeSpan minCacheTime = TimeSpan.FromSeconds(2);
private static readonly TimeSpan defaultObsolescence = TimeSpan.FromMinutes(4);
private class CacheItem
{
internal IDictionary? Data;
internal DateTime DateObsolete;
internal DateTime DateObsoleteTotal;
internal readonly SemaphoreSlim semaphore = new(1);
internal Dictionary<TKey, TEntity> GetData<TKey, TEntity>()
where TKey : notnull
{
if (Data is Dictionary<TKey, TEntity> typedData)
return typedData;
throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique.");
}
internal Dictionary<TKey, TModel> GetData<TKey, TEntity, TModel>(Func<TEntity, TModel> convert, int attempt = 1)
where TKey : notnull
{
if (Data is Dictionary<TKey, TModel> typedData)
return typedData;
if (Data is Dictionary<TKey, TEntity> typedEntityData)
{
if (semaphore.Wait(0))
{
try
{
var convertedData = typedEntityData.ToDictionary(i => i.Key, i => convert(i.Value));
Data = convertedData;
return convertedData;
}
catch
{
throw;
}
finally
{
semaphore.Release();
}
}
else
{
if (semaphore.Wait(semaphoreTimeout))
{
semaphore.Release();
}
else
{
semaphore.Release();
throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while converting cache data");
}
}
}
if (attempt > 0)
return GetData<TKey, TEntity, TModel>(convert, --attempt);
throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique.");
}
}
private static CacheItem GetOrAddCache(string tag, Func<IDictionary> valueFactory, TimeSpan obsolete)
{
CacheItem cache;
while (!caches.ContainsKey(tag))
{
if (semaphore.Wait(0))
{
try
{
cache = new CacheItem();
caches.Add(tag, cache);
}
catch
{
throw;
}
finally
{
semaphore.Release();
}
break;
}
else
{
if (semaphore.Wait(semaphoreTimeout))
{
semaphore.Release();
}
else
{
semaphore.Release();
throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while getting cache");
}
}
}
cache = caches[tag];
if (cache.DateObsolete < DateTime.Now)
{
if (cache.semaphore.Wait(0))
{
try
{
var dateObsolete = DateTime.Now + obsolete;
var dateQueryStart = DateTime.Now;
var data = valueFactory();
var queryTime = DateTime.Now - dateQueryStart;
if (dateObsolete - DateTime.Now < minCacheTime)
dateObsolete = DateTime.Now + minCacheTime;
cache.Data = data;
cache.DateObsolete = dateObsolete;
cache.DateObsoleteTotal = dateObsolete + queryTime + minCacheTime;
}
catch
{
throw;
}
finally
{
cache.semaphore.Release();
}
}
else if (cache.DateObsoleteTotal < DateTime.Now)
{
if (cache.semaphore.Wait(semaphoreTimeout))
{
cache.semaphore.Release();
}
else
{
cache.semaphore.Release();
throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while getting cache");
}
}
}
return cache;
}
private static async Task<CacheItem> GetOrAddCacheAsync(string tag, Func<CancellationToken, Task<IDictionary>> valueFactoryAsync, TimeSpan obsolete, CancellationToken token)
{
CacheItem cache;
while (!caches.ContainsKey(tag))
{
if (semaphore.Wait(0))
{
try
{
cache = new CacheItem();
caches.Add(tag, cache);
}
catch
{
throw;
}
finally
{
semaphore.Release();
}
break;
}
else
{
if (await semaphore.WaitAsync(semaphoreTimeout, token))
{
semaphore.Release();
}
else
{
semaphore.Release();
throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while getting cache");
}
}
}
cache = caches[tag];
if (cache.DateObsolete < DateTime.Now)
{
if (cache.semaphore.Wait(0))
{
try
{
var dateObsolete = DateTime.Now + obsolete;
var dateQueryStart = DateTime.Now;
var data = await valueFactoryAsync(token);
var queryTime = DateTime.Now - dateQueryStart;
if (dateObsolete - DateTime.Now < minCacheTime)
dateObsolete = DateTime.Now + minCacheTime;
cache.Data = data;
cache.DateObsolete = dateObsolete;
cache.DateObsoleteTotal = dateObsolete + queryTime + minCacheTime;
}
catch
{
throw;
}
finally
{
cache.semaphore.Release();
}
}
else if (cache.DateObsoleteTotal < DateTime.Now)
{
if (await cache.semaphore.WaitAsync(semaphoreTimeout, token))
{
cache.semaphore.Release();
}
else
{
cache.semaphore.Release();
throw new TimeoutException("EfCacheL2.GetOrAddCache. Can't wait too long while getting updated cache");
}
}
}
return cache;
}
/// <summary>
/// Кешировать запрос в Dictionary&lt;<typeparamref name="TKey"/>, <typeparamref name="TEntity"/>&gt;. С тегом typeof(TEntity).Name и ключом int Id
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <param name="query"></param>
/// <returns></returns>
public static Dictionary<int, TEntity> FromCacheDictionary<TEntity>(
this IQueryable<TEntity> query)
where TEntity : class, AsbCloudDb.Model.IId
{
var tag = typeof(TEntity).Name;
return FromCacheDictionary(query, tag, defaultObsolescence, e => e.Id);
}
/// <summary>
/// Кешировать запрос в Dictionary&lt;<typeparamref name="TKey"/>, <typeparamref name="TEntity"/>&gt;. С тегом typeof(TEntity).Name
/// </summary>
/// <typeparam name="TKey"></typeparam>
/// <typeparam name="TEntity"></typeparam>
/// <param name="query"></param>
/// <param name="keySelector">Делегат получения ключа из записи</param>
/// <returns></returns>
public static Dictionary<TKey, TEntity> FromCacheDictionary<TKey, TEntity>(
this IQueryable<TEntity> query,
Func<TEntity, TKey> keySelector)
where TEntity : class
where TKey : notnull
{
var tag = typeof(TEntity).Name;
return FromCacheDictionary(query, tag, defaultObsolescence, keySelector);
}
/// <summary>
/// Кешировать запрос в Dictionary&lt;<typeparamref name="TKey"/>, <typeparamref name="TEntity"/>&gt;.
/// </summary>
/// <typeparam name="TKey">тип ключа</typeparam>
/// <typeparam name="TEntity">тип значения</typeparam>
/// <param name="query"></param>
/// <param name="tag">Метка кеша</param>
/// <param name="obsolescence">Период устаревания данных</param>
/// <param name="keySelector">Делегат получения ключа из записи</param>
/// <returns></returns>
public static Dictionary<TKey, TEntity> FromCacheDictionary<TKey, TEntity>(
this IQueryable<TEntity> query,
string tag,
TimeSpan obsolescence,
Func<TEntity, TKey> keySelector)
where TEntity : class
where TKey : notnull
{
IDictionary factory()
=> query.AsNoTracking().ToDictionary(keySelector);
var cache = GetOrAddCache(tag, factory, obsolescence);
return cache.GetData<TKey, TEntity>();
}
/// <summary>
/// Кешировать запрос с последующим преобразованием из <typeparamref name="TEntity"/> в <typeparamref name="TModel"/>.<br/>
/// Преобразование выполняется после получения из БД, результат кешируется в Dictionary&lt;<typeparamref name="TKey"/>, <typeparamref name="TModel"/>&gt;.
/// </summary>
/// <typeparam name="TKey">тип ключа</typeparam>
/// <typeparam name="TEntity">тип значения</typeparam>
/// <typeparam name="TModel"></typeparam>
/// <param name="query"></param>
/// <param name="tag">Метка кеша</param>
/// <param name="obsolescence">Период устаревания данных</param>
/// <param name="keySelector">Делегат получения ключа из записи</param>
/// <param name="convert">Преобразование данных БД в DTO</param>
/// <returns></returns>
public static Dictionary<TKey, TModel> FromCacheDictionary<TKey, TEntity, TModel>(
this IQueryable<TEntity> query,
string tag,
TimeSpan obsolescence,
Func<TEntity, TKey> keySelector,
Func<TEntity, TModel> convert)
where TEntity : class
where TKey : notnull
{
IDictionary factory()
=> query.AsNoTracking().ToDictionary(keySelector);
var cache = GetOrAddCache(tag, factory, obsolescence);
return cache.GetData<TKey, TEntity, TModel>(convert);
}
/// <summary>
/// Асинхронно кешировать запрос в Dictionary&lt;<typeparamref name="TKey"/>, <typeparamref name="TEntity"/>&gt;. С тегом typeof(TEntity).Name и ключом int Id
/// </summary>
/// <typeparam name="TEntity"></typeparam>
/// <param name="query"></param>
/// <param name="token"></param>
/// <returns></returns>
public static Task<Dictionary<int, TEntity>> FromCacheDictionaryAsync<TEntity>(
this IQueryable<TEntity> query,
CancellationToken token = default)
where TEntity : class, AsbCloudDb.Model.IId
{
var tag = typeof(TEntity).Name;
return FromCacheDictionaryAsync(query, tag, defaultObsolescence, e => e.Id, token);
}
/// <summary>
/// Асинхронно кешировать запрос в Dictionary&lt;<typeparamref name="TKey"/>, <typeparamref name="TEntity"/>&gt;. С тегом typeof(TEntity).Name
/// </summary>
/// <typeparam name="TKey"></typeparam>
/// <typeparam name="TEntity"></typeparam>
/// <param name="query"></param>
/// <param name="keySelector">Делегат получения ключа из записи</param>
/// <param name="token"></param>
/// <returns></returns>
public static Task<Dictionary<TKey, TEntity>> FromCacheDictionaryAsync<TKey, TEntity>(
this IQueryable<TEntity> query,
Func<TEntity, TKey> keySelector,
CancellationToken token = default)
where TEntity : class
where TKey : notnull
{
var tag = typeof(TEntity).Name;
return FromCacheDictionaryAsync(query, tag, defaultObsolescence, keySelector, token);
}
/// <summary>
/// Асинхронно кешировать запрос в Dictionary&lt;<typeparamref name="TKey"/>, <typeparamref name="TEntity"/>&gt;.
/// </summary>
/// <typeparam name="TKey">тип ключа</typeparam>
/// <typeparam name="TEntity">тип значения</typeparam>
/// <param name="query"></param>
/// <param name="tag">Метка кеша</param>
/// <param name="obsolescence">Период устаревания данных</param>
/// <param name="keySelector">Делегат получения ключа из записи</param>
/// <param name="token"></param>
/// <returns></returns>
public static async Task<Dictionary<TKey, TEntity>> FromCacheDictionaryAsync<TKey, TEntity>(
this IQueryable<TEntity> query,
string tag,
TimeSpan obsolescence,
Func<TEntity, TKey> keySelector,
CancellationToken token = default)
where TEntity : class
where TKey : notnull
{
async Task<IDictionary> factory(CancellationToken token)
=> await query.AsNoTracking().ToDictionaryAsync(keySelector, token);
var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token);
return cache.GetData<TKey, TEntity>();
}
/// <summary>
/// Асинхронно кешировать запрос с последующим преобразованием из <typeparamref name="TEntity"/> в <typeparamref name="TModel"/>.<br/>
/// Преобразование выполняется после получения из БД, результат кешируется в Dictionary&lt;<typeparamref name="TKey"/>, <typeparamref name="TModel"/>&gt;.
/// </summary>
/// <typeparam name="TKey">тип ключа</typeparam>
/// <typeparam name="TEntity">тип значения</typeparam>
/// <typeparam name="TModel"></typeparam>
/// <param name="query"></param>
/// <param name="tag">Метка кеша</param>
/// <param name="obsolescence">Период устаревания данных</param>
/// <param name="keySelector">Делегат получения ключа из записи</param>
/// <param name="convert">Преобразование данных БД в DTO</param>
/// <param name="token"></param>
/// <returns></returns>
public static async Task<Dictionary<TKey, TModel>> FromCacheDictionaryAsync<TKey, TEntity, TModel>(this IQueryable<TEntity> query, string tag, TimeSpan obsolescence, Func<TEntity, TKey> keySelector, Func<TEntity, TModel> convert, CancellationToken token = default)
where TEntity : class
where TKey : notnull
{
async Task<IDictionary> factory(CancellationToken token)
=> await query.AsNoTracking().ToDictionaryAsync(keySelector, token);
var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token);
return cache.GetData<TKey, TEntity, TModel>(convert);
}
/// <summary>
/// drops cache with tag = typeof(T).Name
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="query"></param>
public static void DropCacheDictionary<T>(this IQueryable<T> query)
{
var tag = typeof(T).Name;
DropCacheDictionary<T>(query, tag);
}
/// <summary>
/// Очистить кеш
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="query"></param>
/// <param name="tag">Метка кеша</param>
public static void DropCacheDictionary<T>(this IQueryable<T> query, string tag)
{
caches.Remove(tag, out var _);
}
}
#nullable disable
}

View File

@ -349,7 +349,7 @@ namespace AsbCloudInfrastructure.EfCache
return cache.GetData(convert);
}
public static Task<IEnumerable<TEntity>> FromCacheAsync<TEntity>(this IQueryable<TEntity> query, CancellationToken token = default)
public static Task<IEnumerable<TEntity>> FromCacheAsync<TEntity>(this IQueryable<TEntity> query, CancellationToken token)
where TEntity : class
{
var tag = typeof(TEntity).Name;
@ -365,7 +365,7 @@ namespace AsbCloudInfrastructure.EfCache
/// <param name="obsolescence">Период устаревания данных</param>
/// <param name="token"></param>
/// <returns></returns>
public static async Task<IEnumerable<TEntity>> FromCacheAsync<TEntity>(this IQueryable<TEntity> query, string tag, TimeSpan obsolescence, CancellationToken token = default)
public static async Task<IEnumerable<TEntity>> FromCacheAsync<TEntity>(this IQueryable<TEntity> query, string tag, TimeSpan obsolescence, CancellationToken token)
where TEntity : class
{
async Task<object[]> factory(CancellationToken token)
@ -386,7 +386,7 @@ namespace AsbCloudInfrastructure.EfCache
/// <param name="convert">Преобразование данных БД в DTO</param>
/// <param name="token"></param>
/// <returns></returns>
public static async Task<IEnumerable<TModel>> FromCacheAsync<TEntity, TModel>(this IQueryable<TEntity> query, string tag, TimeSpan obsolescence, Func<TEntity, TModel> convert, CancellationToken token = default)
public static async Task<IEnumerable<TModel>> FromCacheAsync<TEntity, TModel>(this IQueryable<TEntity> query, string tag, TimeSpan obsolescence, Func<TEntity, TModel> convert, CancellationToken token)
where TEntity : class
{
async Task<object[]> factory(CancellationToken token)

View File

@ -54,7 +54,7 @@ namespace AsbCloudInfrastructure.Repository
public override async Task<IEnumerable<TDto>> GetAllAsync(CancellationToken token)
{
var cache = await GetCacheAsync(token);
return cache.Values;
return cache;
}
/// <summary>
@ -65,14 +65,14 @@ namespace AsbCloudInfrastructure.Repository
public override TDto? GetOrDefault(int id)
{
var cache = GetCache();
return cache.GetValueOrDefault(id);
return cache.FirstOrDefault(d => d.Id == id);
}
/// <inheritdoc/>
public override async Task<TDto?> GetOrDefaultAsync(int id, CancellationToken token)
{
var cache = await GetCacheAsync(token);
return cache.GetValueOrDefault(id);
return cache.FirstOrDefault(d => d.Id == id);
}
/// <inheritdoc/>
@ -102,17 +102,17 @@ namespace AsbCloudInfrastructure.Repository
return result;
}
protected virtual Task<Dictionary<int, TDto>> GetCacheAsync(CancellationToken token)
protected virtual Task<IEnumerable<TDto>> GetCacheAsync(CancellationToken token)
=> GetQuery()
.FromCacheDictionaryAsync(CacheTag, CacheOlescence, KeySelector, Convert, token);
.FromCacheAsync(CacheTag, CacheOlescence, Convert, token);
protected virtual Dictionary<int, TDto> GetCache()
protected virtual IEnumerable<TDto> GetCache()
=> GetQuery()
.FromCacheDictionary(CacheTag, CacheOlescence, KeySelector, Convert);
.FromCache(CacheTag, CacheOlescence, Convert);
protected virtual void DropCache()
=> dbSet.DropCacheDictionary(CacheTag);
=> dbSet.DropCache(CacheTag);
}
#nullable disable
}

View File

@ -23,25 +23,25 @@ namespace AsbCloudInfrastructure.Repository
public CrudWellRelatedCacheServiceBase(IAsbCloudDbContext context, Func<DbSet<TEntity>, IQueryable<TEntity>> makeQuery)
: base(context, makeQuery) { }
public async Task<IEnumerable<TDto>> GetByIdWellAsync(int idWell, CancellationToken token)
public async Task<IEnumerable<TDto>?> GetByIdWellAsync(int idWell, CancellationToken token)
{
var cache = await GetCacheAsync(token);
var dtos = cache.Values
var dtos = cache
.Where(e => e.IdWell == idWell)
.ToList();
return dtos;
}
public async Task<IEnumerable<TDto>> GetByIdWellAsync(IEnumerable<int> idsWells, CancellationToken token)
public async Task<IEnumerable<TDto>?> GetByIdWellAsync(IEnumerable<int> idsWells, CancellationToken token)
{
if (!idsWells.Any())
return Enumerable.Empty<TDto>();
var cache = await GetCacheAsync(token);
var dtos = cache.Values
var dtos = cache
.Where(e => idsWells.Contains(e.IdWell))
.ToList();
return dtos;

View File

@ -23,7 +23,7 @@ namespace AsbCloudInfrastructure.Repository
public CrudWellRelatedServiceBase(IAsbCloudDbContext context, Func<DbSet<TEntity>, IQueryable<TEntity>> makeQuery)
: base(context, makeQuery) { }
public async Task<IEnumerable<TDto>> GetByIdWellAsync(int idWell, CancellationToken token)
public async Task<IEnumerable<TDto>?> GetByIdWellAsync(int idWell, CancellationToken token)
{
var entities = await GetQuery()
.Where(e => e.IdWell == idWell)
@ -32,7 +32,7 @@ namespace AsbCloudInfrastructure.Repository
return dtos;
}
public async Task<IEnumerable<TDto>> GetByIdWellAsync(IEnumerable<int> idsWells, CancellationToken token)
public async Task<IEnumerable<TDto>?> GetByIdWellAsync(IEnumerable<int> idsWells, CancellationToken token)
{
if (!idsWells.Any())
return Enumerable.Empty<TDto>();

View File

@ -12,6 +12,7 @@ using AsbCloudApp.Data.DailyReport;
using AsbCloudApp.Requests;
using AsbCloudInfrastructure.Services.DetectOperations;
using AsbCloudApp.Data.DetectedOperation;
using AsbCloudApp.Exceptions;
namespace AsbCloudInfrastructure.Services.DailyReport
{
@ -34,7 +35,8 @@ namespace AsbCloudInfrastructure.Services.DailyReport
{
var well = wellService.GetOrDefault(idWell);
if (well is null || well.Timezone is null)
return null;
throw new ArgumentInvalidException("idWell doesn`t exist", nameof(idWell));
var query = db.DailyReports.Where(r => r.IdWell == idWell);
DateTimeOffset ExtractDate(DateTime dateTime)
@ -65,9 +67,8 @@ namespace AsbCloudInfrastructure.Services.DailyReport
public async Task<DailyReportDto> GetOrGenerateAsync(int idWell, DateTime date, CancellationToken token)
{
var dateOnly = DateTime.SpecifyKind(date.Date, DateTimeKind.Utc);
var dailyReportDto = await GetAsync(idWell, dateOnly, token);
if (dailyReportDto is null)
dailyReportDto = await MakeDefaultDailyReportAsync(idWell, dateOnly, token);
var dailyReportDto = await GetOrDefaultAsync(idWell, dateOnly, token);
dailyReportDto ??= await MakeDefaultDailyReportAsync(idWell, dateOnly, token);
return dailyReportDto;
}
@ -105,7 +106,7 @@ namespace AsbCloudInfrastructure.Services.DailyReport
public async Task<Stream?> MakeReportAsync(int idWell, DateTime date, CancellationToken token = default)
{
var dailyReportDto = await GetAsync(idWell, date, token);
var dailyReportDto = await GetOrDefaultAsync(idWell, date, token);
if (dailyReportDto is null)
return null;
@ -113,7 +114,7 @@ namespace AsbCloudInfrastructure.Services.DailyReport
return memoryStream;
}
private async Task<DailyReportDto?> GetAsync(int idWell, DateTime date, CancellationToken token)
private async Task<DailyReportDto?> GetOrDefaultAsync(int idWell, DateTime date, CancellationToken token)
{
var dateOffset = date.Date;
var entity = await db.DailyReports

View File

@ -27,7 +27,7 @@ namespace AsbCloudInfrastructure.Services
public async Task<DrillParamsDto?> GetDefaultDrillParamsAsync(int idWell,
double startDepth, double endDepth, CancellationToken token = default)
{
var idTelemetry = telemetryService.GetIdTelemetryByIdWell(idWell);
var idTelemetry = telemetryService.GetOrDefaultIdTelemetryByIdWell(idWell);
if (idTelemetry is null)
return null;

View File

@ -15,15 +15,15 @@ namespace AsbCloudInfrastructure.Services
: base(context)
{
}
//TODO: Перенести в сервис "дело скважины"
public async Task<IEnumerable<FileCategoryDto>> GetWellCaseCategoriesAsync(CancellationToken token)
{
var cache = await GetCacheAsync(token)
.ConfigureAwait(false);
var dtos = cache
.Where(kv => kv.Key >= 10000)
.Where(kv => kv.Key <= 20000)
.Select(kv => kv.Value);
.Where(f => f.Id >= 10000)
.Where(f => f.Id <= 20000);
return dtos;
}

View File

@ -113,7 +113,7 @@ namespace AsbCloudInfrastructure.Services
public DatesRangeDto GetDatesRangeOrDefault(int idWell)
{
var idTelemetry = telemetryService.GetIdTelemetryByIdWell(idWell);
var idTelemetry = telemetryService.GetOrDefaultIdTelemetryByIdWell(idWell);
if (idTelemetry is null)
return null;
var range = telemetryService.GetDatesRange((int)idTelemetry);

View File

@ -34,7 +34,7 @@ namespace AsbCloudInfrastructure.Services.SAUB
int take = 32,
CancellationToken token = default)
{
var idTelemetry = telemetryService.GetIdTelemetryByIdWell(idWell);
var idTelemetry = telemetryService.GetOrDefaultIdTelemetryByIdWell(idWell);
if (idTelemetry is null)
return null;

View File

@ -10,26 +10,31 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
#nullable enable
namespace AsbCloudInfrastructure.Services.SAUB
{
public abstract class TelemetryDataBaseService<TDto, TModel> : ITelemetryDataService<TDto>
public abstract class TelemetryDataBaseService<TDto, TEntity> : ITelemetryDataService<TDto>
where TDto : AsbCloudApp.Data.ITelemetryData
where TModel : class, ITelemetryData
where TEntity : class, ITelemetryData
{
protected readonly IAsbCloudDbContext db;
private readonly ITelemetryService telemetryService;
protected readonly CacheTable<TelemetryUser> cacheTelemetryUsers;
private readonly TelemetryDataCache<TDto> telemetryDataCache;
public TelemetryDataBaseService(
IAsbCloudDbContext db,
ITelemetryService telemetryService,
TelemetryDataCache<TDto> telemetryDataCache,
CacheDb cacheDb)
{
this.db = db;
this.telemetryService = telemetryService;
cacheTelemetryUsers = cacheDb.GetCachedTable<TelemetryUser>((AsbCloudDbContext)db);
this.telemetryDataCache = telemetryDataCache;
}
/// <inheritdoc/>
public virtual async Task<int> UpdateDataAsync(string uid, IEnumerable<TDto> dtos, CancellationToken token = default)
{
if (dtos == default || !dtos.Any())
@ -53,6 +58,8 @@ namespace AsbCloudInfrastructure.Services.SAUB
var idTelemetry = telemetryService.GetOrCreateTelemetryIdByUid(uid);
var timezone = telemetryService.GetTimezone(idTelemetry);
telemetryDataCache.AddRange(idTelemetry, dtos);
var entities = dtosList.Select(dto =>
{
var entity = Convert(dto, timezone.Hours);
@ -63,7 +70,7 @@ namespace AsbCloudInfrastructure.Services.SAUB
var entityMaxDate = entities.Max(e => e.DateTime);
telemetryService.TelemetryTracker.SaveRequestDate(uid, entityMaxDate);
var dbset = db.Set<TModel>();
var dbset = db.Set<TEntity>();
var stopwatch = Stopwatch.StartNew();
try
{
@ -83,11 +90,14 @@ namespace AsbCloudInfrastructure.Services.SAUB
}
}
public virtual async Task<IEnumerable<TDto>> GetAsync(int idWell,
// TODO: It shouldn`t be nullable. Throw exceptions instead and return empty.
/// <inheritdoc/>
public virtual async Task<IEnumerable<TDto>?> GetOrDefaultAsync(int idWell,
DateTime dateBegin = default, double intervalSec = 600d,
int approxPointsCount = 1024, CancellationToken token = default)
{
var idTelemetry = telemetryService.GetIdTelemetryByIdWell(idWell) ?? -1;
var idTelemetry = telemetryService.GetOrDefaultIdTelemetryByIdWell(idWell) ?? -1;
if (idTelemetry == -1)
return null;
@ -110,8 +120,12 @@ namespace AsbCloudInfrastructure.Services.SAUB
if (dateBeginUtc == default)
dateBeginUtc = DateTime.UtcNow.AddSeconds(-intervalSec);
var cacheData = telemetryDataCache.GetOrDefault(idTelemetry, dateBeginUtc.ToRemoteDateTime(timezone.Hours), intervalSec, approxPointsCount);
if (cacheData is not null)
return cacheData;
var dateEnd = dateBeginUtc.AddSeconds(intervalSec);
var dbSet = db.Set<TModel>();
var dbSet = db.Set<TEntity>();
var query = dbSet
.Where(d => d.IdTelemetry == idTelemetry
@ -144,9 +158,10 @@ namespace AsbCloudInfrastructure.Services.SAUB
return dtos;
}
public abstract TDto Convert(TModel src, double timezoneOffset);
public abstract TDto Convert(TEntity src, double timezoneOffset);
public abstract TModel Convert(TDto src, double timezoneOffset);
public abstract TEntity Convert(TDto src, double timezoneOffset);
}
}
#nullable disable

View File

@ -0,0 +1,184 @@
using AsbCloudDb.Model;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Mapster;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
#nullable enable
namespace AsbCloudInfrastructure.Services.SAUB
{
public class TelemetryDataCache<TDto>
where TDto : AsbCloudApp.Data.ITelemetryData
{
private const int activeWellCapacity = 24 * 60 * 60;
private const int doneWellCapacity = 65 * 60;
private readonly ConcurrentDictionary<int, CyclycArray<TDto>> caches;
private bool isLoading = false;
private TelemetryDataCache()
{
caches = new();
}
private static TelemetryDataCache<TDto>? instance;
//TODO: Move initialize fromDB to bacground service task
public static TelemetryDataCache<TDto> GetInstance<TEntity>(IConfiguration configuration)
where TEntity : class, ITelemetryData
{
if (instance is null)
{
instance = new TelemetryDataCache<TDto>();
_ = Task.Run(() =>
{
using var db = MakeContext(configuration);
instance.InitializeCacheFromDB<TEntity>(db);
});
}
return instance;
}
public static TelemetryDataCache<TDto> GetInstance<TEntity>(IAsbCloudDbContext db, out Task initializationTask)
where TEntity : class, ITelemetryData
{
if (instance is null)
{
instance = new TelemetryDataCache<TDto>();
initializationTask = Task.Run(() =>
{
instance.InitializeCacheFromDB<TEntity>(db);
});
}
else
initializationTask = Task.CompletedTask;
return instance;
}
/// <summary>
/// Добавить новые элементы в кеш
/// </summary>
/// <param name="idTelemetry"></param>
/// <param name="range"></param>
public void AddRange(int idTelemetry, IEnumerable<TDto> range)
{
CyclycArray<TDto> cacheItem;
if (isLoading)
{
if (caches.TryGetValue(idTelemetry, out CyclycArray<TDto>? localCacheItem))
cacheItem = localCacheItem;
else
return;
}
else
{
cacheItem = caches.GetOrAdd(idTelemetry, _ => new CyclycArray<TDto>(activeWellCapacity));
}
var newItems = range
.OrderBy(i => i.DateTime);
foreach (var item in newItems)
item.IdTelemetry = idTelemetry;
cacheItem.AddRange(newItems);
}
/// <summary>
/// Получить данные из кеша. <br/>
/// Если dateBegin меньше минимального элемента в кеше, то вернется null.
/// Даже если intervalSec частично перекрыт данными из кеша.
/// </summary>
/// <param name="idTelemetry"></param>
/// <param name="dateBegin"></param>
/// <param name="intervalSec"></param>
/// <param name="approxPointsCount">кол-во элементов до которых эти данные прореживаются</param>
/// <returns></returns>
public IEnumerable<TDto>? GetOrDefault(int idTelemetry, DateTime dateBegin, double intervalSec = 600d, int approxPointsCount = 1024)
{
if(!caches.TryGetValue(idTelemetry, out CyclycArray<TDto>? cacheItem))
return null;
if (cacheItem is null || !cacheItem.Any() || cacheItem[0].DateTime > dateBegin)
return null;
var dateEnd = dateBegin.AddSeconds(intervalSec);
var items = cacheItem
.Where(i => i.DateTime >= dateBegin && i.DateTime <= dateEnd);
var ratio = items.Count() / approxPointsCount;
if (ratio > 1)
items = items
.Where((_, index) => index % ratio == 0);
return items;
}
private void InitializeCacheFromDB<TEntity>(IAsbCloudDbContext db)
where TEntity : class, ITelemetryData
{
if (isLoading)
throw new Exception("Multiple cache loading detected.");
isLoading = true;
Well[] wells = Array.Empty<Well>();
wells = db.Set<Well>()
.Include(well => well.Telemetry)
.Include(well => well.Cluster)
.Where(well => well.IdTelemetry != null)
.ToArray();
foreach (Well well in wells)
{
var capacity = well.IdState == 1
? activeWellCapacity
: doneWellCapacity;
var idTelemetry = well.IdTelemetry!.Value;
var hoursOffset = well.Timezone.Hours;
IEnumerable<TDto> cacheItemData = GetCacheDataFromDb<TEntity>(db, idTelemetry, capacity, hoursOffset);
var cacheItem = new CyclycArray<TDto>(capacity);
cacheItem.AddRange(cacheItemData);
caches.TryAdd(idTelemetry, cacheItem);
System.Diagnostics.Trace.TraceInformation($"cache<{typeof(TDto).Name}> for well: {well.Cluster?.Caption}/{well.Caption} loaded");
}
System.Diagnostics.Trace.TraceInformation($"cache<{typeof(TDto).Name}> load complete");
isLoading = false;
}
private static IAsbCloudDbContext MakeContext(IConfiguration configuration)
{
var connectionString = configuration.GetConnectionString("DefaultConnection");
var options = new DbContextOptionsBuilder<AsbCloudDbContext>()
.UseNpgsql(connectionString)
.Options;
var db = new AsbCloudDbContext(options);
return db;
}
private static IEnumerable<TDto> GetCacheDataFromDb<TEntity>(IAsbCloudDbContext db, int idTelemetry, int capacity, double hoursOffset)
where TEntity : class, ITelemetryData
{
var entities = db.Set<TEntity>()
.Where(i => i.IdTelemetry == idTelemetry)
.OrderByDescending(i => i.DateTime)
.Take(capacity)
.ToArray()
.AsEnumerable()
.Reverse();
var dtos = entities.Select(entity => {
var dto = entity.Adapt<TDto>();
dto.DateTime = entity.DateTime.ToRemoteDateTime(hoursOffset);
return dto;
});
return dtos;
}
}
}
#nullable disable

View File

@ -16,8 +16,9 @@ namespace AsbCloudInfrastructure.Services.SAUB
public TelemetryDataSaubService(
IAsbCloudDbContext db,
ITelemetryService telemetryService,
TelemetryDataCache<TelemetryDataSaubDto> telemetryDataCache,
CacheDb cacheDb)
: base(db, telemetryService, cacheDb)
: base(db, telemetryService, telemetryDataCache, cacheDb)
{ }
public override TelemetryDataSaub Convert(TelemetryDataSaubDto src, double timezoneOffset)

View File

@ -13,8 +13,9 @@ namespace AsbCloudInfrastructure.Services.SAUB
public TelemetryDataSpinService(
IAsbCloudDbContext db,
ITelemetryService telemetryService,
TelemetryDataCache<TelemetryDataSpinDto> telemetryDataCache,
CacheDb cacheDb)
: base(db, telemetryService, cacheDb)
: base(db, telemetryService, telemetryDataCache, cacheDb)
{ }
public override TelemetryDataSpin Convert(TelemetryDataSpinDto src, double timezoneOffset)

View File

@ -37,22 +37,22 @@ namespace AsbCloudInfrastructure.Services.SAUB
this.timezoneService = timezoneService;
}
private Dictionary<int, Telemetry> GetTelemetryCache()
private IEnumerable<Telemetry> GetTelemetryCache()
{
var cache = db.Telemetries
.Include(t => t.Well)
.FromCacheDictionary(telemetryCacheTag, telemetryCacheObsolescence, t => t.Id);
.FromCache(telemetryCacheTag, telemetryCacheObsolescence);
return cache;
}
private void DropTelemetryCache()
{
db.Telemetries.DropCacheDictionary(telemetryCacheTag);
db.Telemetries.DropCache(telemetryCacheTag);
}
public DateTime GetLastTelemetryDate(int idTelemetry)
{
var telemetry = GetTelemetryCache().GetValueOrDefault(idTelemetry);
var telemetry = GetTelemetryCache().FirstOrDefault(t => t.Id == idTelemetry);
if (telemetry is null)
throw new Exception($"Telemetry id:{idTelemetry} does not exist");
@ -65,7 +65,7 @@ namespace AsbCloudInfrastructure.Services.SAUB
public DatesRangeDto GetDatesRange(int idTelemetry)
{
var telemetry = GetTelemetryCache().GetValueOrDefault(idTelemetry);
var telemetry = GetTelemetryCache().FirstOrDefault(t => t.Id == idTelemetry);
if (telemetry is null)
throw new Exception($"Telemetry id:{idTelemetry} does not exist");
@ -107,7 +107,7 @@ namespace AsbCloudInfrastructure.Services.SAUB
public SimpleTimezoneDto GetTimezone(int idTelemetry)
{
var telemetry = GetTelemetryCache().GetValueOrDefault(idTelemetry);
var telemetry = GetTelemetryCache().FirstOrDefault(t => t.Id == idTelemetry);
if (telemetry is null)
throw new Exception($"Telemetry id: {idTelemetry} does not exist.");
@ -141,10 +141,10 @@ namespace AsbCloudInfrastructure.Services.SAUB
throw new Exception($"Telemetry id: {idTelemetry} can't find timezone.");
}
public int? GetIdTelemetryByIdWell(int idWell)
public int? GetOrDefaultIdTelemetryByIdWell(int idWell)
{
var telemetry = GetTelemetryCache()
.FirstOrDefault(t => t.Value.Well?.Id == idWell).Value;
.FirstOrDefault(t => t.Well?.Id == idWell);
return telemetry?.Id;
}
@ -156,7 +156,7 @@ namespace AsbCloudInfrastructure.Services.SAUB
private Telemetry? GetOrDefaultTelemetryByUid(string uid)
{
var telemetry = GetTelemetryCache().FirstOrDefault(kv => kv.Value.RemoteUid == uid).Value;
var telemetry = GetTelemetryCache().FirstOrDefault(t => t.RemoteUid == uid);
return telemetry;
}

View File

@ -81,8 +81,7 @@ namespace AsbCloudInfrastructure.Services
.Select(r => r.IdWell);
var wellsDtos = (await GetCacheAsync(token))
.Where(kv => wellsIds.Contains(kv.Key))
.Select(kv => kv.Value);
.Where(w => wellsIds.Contains(w.Id));
return wellsDtos.ToList();
}
@ -95,7 +94,7 @@ namespace AsbCloudInfrastructure.Services
if (dto.IdState is < 0 or > 2)
throw new ArgumentInvalidException("Текущее состояние работы скважины указано неправильно.", nameof(dto));
if (dto.Id != 0 && (await GetCacheAsync(token)).ContainsKey(dto.Id))
if (dto.Id != 0 && (await GetCacheAsync(token)).Any(w => w.Id == dto.Id))
throw new ArgumentInvalidException($"Нельзя повторно добавить скважину с id: {dto.Id}", nameof(dto));
var entity = Convert(dto);
@ -194,7 +193,7 @@ namespace AsbCloudInfrastructure.Services
var cache = await GetCacheAsync(token);
var clusterWellsIds = cache.Values
var clusterWellsIds = cache
.Where((w) => w.IdCluster == well.IdCluster)
.Select(w => w.Id);
@ -247,7 +246,7 @@ namespace AsbCloudInfrastructure.Services
public async Task EnshureTimezonesIsSetAsync(CancellationToken token)
{
var cache = await GetCacheAsync(token);
if (!cache.Values.Any(w => w.Timezone is null))
if (!cache.Any(w => w.Timezone is null))
return;
var defaultTimeZone = new SimpleTimezone

View File

@ -0,0 +1,93 @@
using AsbCloudDb.Model;
using AsbCloudInfrastructure.Services.SAUB;
using Moq;
using System;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
namespace AsbCloudWebApi.Tests.CacheTests
{
public class TelemetryDataCacheTest
{
private const int IdTelemetryOk = 10;
private static readonly DateTime baseDate = DateTime.Now;
private static readonly SimpleTimezone timezone = new() { Hours = TimeZoneInfo.Local.BaseUtcOffset.TotalHours };
private static readonly Well[] wellData = new Well[]{
new(){ Id = 1, IdTelemetry = IdTelemetryOk, IdState = 1, Caption = "", Timezone = timezone}
};
private static readonly TTelemetryData[] telemetryData = new TTelemetryData[]{
new ( IdTelemetryOk, baseDate.AddSeconds(1), timezone.Hours ),
new ( IdTelemetryOk, baseDate.AddSeconds(2), timezone.Hours ),
new ( IdTelemetryOk, baseDate.AddSeconds(3), timezone.Hours ),
new ( IdTelemetryOk, baseDate.AddSeconds(4), timezone.Hours ),
};
private readonly Mock<IAsbCloudDbContext> dbMock;
private TelemetryDataCache<TTelemetryData> cacheTest;
public class TTelemetryData : ITelemetryData, AsbCloudApp.Data.ITelemetryData
{
private DateTimeOffset _dateTime;
public TTelemetryData(int idTelemetry, DateTime dateTime, double hoursOffset)
{
IdTelemetry = idTelemetry;
DateTime = dateTime;
var offset = TimeSpan.FromHours(hoursOffset);
var dateTimeUTC = DateTime.SpecifyKind(dateTime, DateTimeKind.Unspecified);
_dateTime = new DateTimeOffset(dateTimeUTC, offset)
.ToUniversalTime();
}
public int IdTelemetry { get; set; }
public DateTime DateTime { get; set; }
DateTimeOffset ITelemetryData.DateTime { get => _dateTime; set => _dateTime = value; }
}
public TelemetryDataCacheTest()
{
dbMock = new Mock<IAsbCloudDbContext>();
dbMock
.AddDbSetMock(wellData)
.AddDbSetMock(telemetryData);
cacheTest = TelemetryDataCache<TTelemetryData>.GetInstance<TTelemetryData>(dbMock.Object, out Task cacheInitialization);
cacheInitialization.Wait();
}
[Fact]
public void Get_existing_cache_returns_some()
{
var startDate = baseDate.AddSeconds(3);
var data = cacheTest.GetOrDefault(IdTelemetryOk, startDate, 600, 600);
Assert.NotNull(data);
Assert.NotEmpty(data);
}
[Fact]
public void Get_non_existing_cache_returns_null()
{
var startDate = baseDate.AddSeconds(-1);
var data = cacheTest.GetOrDefault(IdTelemetryOk, startDate, 600, 600);
Assert.Null(data);
}
[Fact]
public void Add_new_data_should_replace_IdTelemetry()
{
var startDate = baseDate.AddSeconds(4);
var newTelemetryData = new TTelemetryData[]{
new ( 0, startDate, timezone.Hours),
new ( 0, startDate.AddSeconds(1), timezone.Hours),
};
cacheTest.AddRange(IdTelemetryOk, newTelemetryData);
var data = cacheTest.GetOrDefault(IdTelemetryOk, startDate, 600, 600);
Assert.NotNull(data);
Assert.True(data?.Count() > 2);
Assert.All(data!, p => Assert.Equal(IdTelemetryOk, p.IdTelemetry));
}
}
}

View File

@ -9,7 +9,42 @@ namespace AsbCloudWebApi.Tests.Middlware
{
public class UserConnectionsLimitMiddlwareTest
{
private readonly HttpClient httpClient;
const int iterations2Block = 8;
//Данные в тестовой БД
//select
// tw.id,
// t_stat.minDate,
// t_stat.maxDate
//from(
// select
// id_telemetry,
// count(1) as count,
// min("date") as minDate,
// max("date") as maxDate
// from t_telemetry_data_saub
// group by id_telemetry
//) as t_stat
//join t_well tw on tw.id_telemetry = t_stat.id_telemetry
//where tw is not null
//order by t_stat.count
//limit 10;
private readonly (int, DateTime, DateTime)[] wells = new[]
{
(191, new DateTime(2022, 09, 01, 21, 43, 00, DateTimeKind.Utc), new DateTime(2022, 09, 04, 07, 37, 31, DateTimeKind.Utc)),
(3 , new DateTime(2021, 09, 16, 06, 13, 33, DateTimeKind.Utc), new DateTime(2021, 09, 20, 00, 29, 28, DateTimeKind.Utc)),
(199, new DateTime(2022, 09, 15, 11, 27, 18, DateTimeKind.Utc), new DateTime(2022, 09, 20, 14, 00, 23, DateTimeKind.Utc)),
(6 , new DateTime(2021, 09, 20, 00, 35, 03, DateTimeKind.Utc), new DateTime(2021, 09, 25, 06, 46, 17, DateTimeKind.Utc)),
(41 , new DateTime(2021, 12, 10, 00, 59, 52, DateTimeKind.Utc), new DateTime(2022, 10, 31, 15, 29, 24, DateTimeKind.Utc)),
(100, new DateTime(2022, 04, 24, 03, 04, 05, DateTimeKind.Utc), new DateTime(2022, 04, 29, 11, 38, 36, DateTimeKind.Utc)),
(154, new DateTime(2022, 03, 28, 10, 09, 14, DateTimeKind.Utc), new DateTime(2022, 06, 14, 15, 01, 12, DateTimeKind.Utc)),
(5 , new DateTime(2021, 09, 25, 08, 09, 37, DateTimeKind.Utc), new DateTime(2021, 10, 01, 14, 39, 51, DateTimeKind.Utc)),
(1 , new DateTime(2021, 09, 10, 01, 32, 42, DateTimeKind.Utc), new DateTime(2021, 09, 18, 00, 35, 22, DateTimeKind.Utc)),
(112, new DateTime(2022, 04, 20, 16, 47, 51, DateTimeKind.Utc), new DateTime(2022, 04, 28, 15, 04, 33, DateTimeKind.Utc)),
};
public UserConnectionsLimitMiddlwareTest()
{
@ -20,66 +55,53 @@ namespace AsbCloudWebApi.Tests.Middlware
})
.Build();
host.Start();
httpClient = new ();
httpClient.DefaultRequestHeaders.Authorization = new("Bearer", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiZGV2IiwiaWRDb21wYW55IjoiMSIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6InJvb3QiLCJuYmYiOjE2NjY1ODY2MjAsImV4cCI6MTY5ODE0NDIyMCwiaXNzIjoiYSIsImF1ZCI6ImEifQ.zqBdR4nYB87-Xyzv025waasN47i43c9FJ23RfzIvUsM");
}
[Fact]
public async Task Send_n_requests_and_get_blocked()
{
//Данные в тестовой БД
//select
// tw.id,
// t_stat.minDate,
// t_stat.maxDate
//from(
// select
// id_telemetry,
// count(1) as count,
// min("date") as minDate,
// max("date") as maxDate
// from t_telemetry_data_saub
// group by id_telemetry
//) as t_stat
//join t_well tw on tw.id_telemetry = t_stat.id_telemetry
//where tw is not null
//order by t_stat.count
//limit 10;
var wells = new []
{
(191, new DateTime(2022, 09, 01, 21, 43, 00, DateTimeKind.Utc), new DateTime(2022, 09, 04, 07, 37, 31, DateTimeKind.Utc)),
(3 , new DateTime(2021, 09, 16, 06, 13, 33, DateTimeKind.Utc), new DateTime(2021, 09, 20, 00, 29, 28, DateTimeKind.Utc)),
(199, new DateTime(2022, 09, 15, 11, 27, 18, DateTimeKind.Utc), new DateTime(2022, 09, 20, 14, 00, 23, DateTimeKind.Utc)),
(6 , new DateTime(2021, 09, 20, 00, 35, 03, DateTimeKind.Utc), new DateTime(2021, 09, 25, 06, 46, 17, DateTimeKind.Utc)),
(41 , new DateTime(2021, 12, 10, 00, 59, 52, DateTimeKind.Utc), new DateTime(2022, 10, 31, 15, 29, 24, DateTimeKind.Utc)),
(100, new DateTime(2022, 04, 24, 03, 04, 05, DateTimeKind.Utc), new DateTime(2022, 04, 29, 11, 38, 36, DateTimeKind.Utc)),
(154, new DateTime(2022, 03, 28, 10, 09, 14, DateTimeKind.Utc), new DateTime(2022, 06, 14, 15, 01, 12, DateTimeKind.Utc)),
(5 , new DateTime(2021, 09, 25, 08, 09, 37, DateTimeKind.Utc), new DateTime(2021, 10, 01, 14, 39, 51, DateTimeKind.Utc)),
(1 , new DateTime(2021, 09, 10, 01, 32, 42, DateTimeKind.Utc), new DateTime(2021, 09, 18, 00, 35, 22, DateTimeKind.Utc)),
(112, new DateTime(2022, 04, 20, 16, 47, 51, DateTimeKind.Utc), new DateTime(2022, 04, 28, 15, 04, 33, DateTimeKind.Utc)),
};
var i = 0;
for (; i < 5; i++)
for (; i < iterations2Block; i++)
_ = Task.Run(async () =>
{
var well = wells[i];
var url = MakeUrl(well.Item1, well.Item2, well.Item3);
var response = await httpClient.GetAsync(url);
var response = await MakeHttpClient().GetAsync(url);
//await response.Content.ReadAsStringAsync();
await Task.Delay(1000);
});
var well = wells[i];
var url = MakeUrl(well.Item1, well.Item2, well.Item3);
var response = await httpClient.GetAsync(url);
var response = await MakeHttpClient().GetAsync(url);
Assert.Equal(System.Net.HttpStatusCode.TooManyRequests, response.StatusCode);
}
[Fact]
public async Task Send_n_requests_and_get_blocked_then_restored()
{
var i = 0;
var tasks = new Task[iterations2Block];
for (; i < iterations2Block; i++)
tasks[i] = Task.Run(async () =>
{
var well = wells[i];
var url = MakeUrl(well.Item1, well.Item2, well.Item3);
var response = await MakeHttpClient().GetAsync(url);
await Task.Delay(1000);
});
var well = wells[i];
var url = MakeUrl(well.Item1, well.Item2, well.Item3);
var response = await MakeHttpClient().GetAsync(url);
Assert.Equal(System.Net.HttpStatusCode.TooManyRequests, response.StatusCode);
Task.WaitAll(tasks);
response = await MakeHttpClient().GetAsync(url);
Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode);
}
private static string MakeUrl(int idWell, DateTime dateBegin, DateTime dateEnd)
{
var interval = (dateEnd - dateBegin).TotalSeconds;
@ -87,5 +109,13 @@ namespace AsbCloudWebApi.Tests.Middlware
var url = $"http://127.0.0.1:5000/api/TelemetryDataSaub/{idWell}?begin={dateBeginString}&intervalSec={interval}&approxPointsCount={interval}";
return url;
}
private static HttpClient MakeHttpClient()
{
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new("Bearer", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiZGV2IiwiaWRDb21wYW55IjoiMSIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6InJvb3QiLCJuYmYiOjE2NjY1ODY2MjAsImV4cCI6MTY5ODE0NDIyMCwiaXNzIjoiYSIsImF1ZCI6ImEifQ.zqBdR4nYB87-Xyzv025waasN47i43c9FJ23RfzIvUsM");
return httpClient;
}
}
}

View File

@ -69,7 +69,7 @@ public class ClusterServiceTest
public ClusterServiceTest()
{
context = TestHelpter.MakeTestContext();
context = TestHelpter.MakeRealTestContext();
wellService = new Mock<IWellService>();
context.Deposits.RemoveRange(context.Deposits);
context.Clusters.RemoveRange(context.Clusters);

View File

@ -21,7 +21,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
protected override ICrudService<DepositDto> MakeService()
{
var dbContext = TestHelpter.MakeTestContext();
var dbContext = TestHelpter.MakeRealTestContext();
return new CrudCacheServiceBase<DepositDto, Deposit>(dbContext);
}
}

View File

@ -107,7 +107,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
public DetectedOperationServiceTest()
{
context = TestHelpter.MakeTestContext();
context = TestHelpter.MakeRealTestContext();
context.SaveChanges();
context.Telemetries.Add(telemetry);

View File

@ -90,7 +90,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
{
AsbCloudInfrastructure.DependencyInjection.MapsterSetup();
db = TestHelpter.MakeTestContext();
db = TestHelpter.MakeRealTestContext();
db.Wells.AddRange(wells);
db.Companies.AddRange(companies);
db.SaveChanges();

View File

@ -19,7 +19,7 @@ public class EventServiceTest
public EventServiceTest()
{
context = TestHelpter.MakeTestContext();
context = TestHelpter.MakeRealTestContext();
cacheDb = new CacheDb();
var telemetryTracker = new Mock<ITelemetryTracker>();
var imezoneServiceMock = new Mock<ITimezoneService>();

View File

@ -17,7 +17,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
public FileCategoryServiceTest()
{
context = TestHelpter.MakeTestContext();
context = TestHelpter.MakeRealTestContext();
context.SaveChanges();
service = new FileCategoryService(context);
}

View File

@ -58,7 +58,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
public ScheduleServiceTest()
{
context = TestHelpter.MakeTestContext();
context = TestHelpter.MakeRealTestContext();
context.SaveChanges();
context.Deposits.Add(deposit);

View File

@ -40,7 +40,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
timezoneService.Setup(s => s.GetByCoordinates(It.IsAny<double>(), It.IsAny<double>()))
.Returns(timezone);
context = TestHelpter.MakeTestContext();
context = TestHelpter.MakeRealTestContext();
cacheDb = new CacheDb();
telemetryService = new TelemetryService(context, telemetryTracker.Object, timezoneService.Object);
@ -64,7 +64,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
public async Task UpdateDataAsync()
{
// Arrange
var telemetryDataSaubService = new TelemetryDataSaubService(context, telemetryService, cacheDb);
var telemetryDataSaubService = new TelemetryDataSaubService(context, telemetryService, null, cacheDb);
var now = DateTimeOffset.UtcNow.ToOffset(TimeSpan.FromHours(timezone.Hours)).DateTime;
var tuser = "Завулон";

View File

@ -43,7 +43,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
public WellFinalDocumentsServiceTest()
{
context = TestHelpter.MakeTestContext();
context = TestHelpter.MakeRealTestContext();
context.SaveChanges();
fileServiceMock = new Mock<FileService>();

View File

@ -1,7 +1,8 @@
using AsbCloudApp.Services;
using AsbCloudDb.Model;
using AsbCloudDb.Model;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Moq;
using System.Collections.Generic;
using System.Linq;
namespace AsbCloudWebApi.Tests
{
@ -9,7 +10,7 @@ namespace AsbCloudWebApi.Tests
{
// Попробовать когда-нибудь https://github.com/MichalJankowskii/Moq.EntityFrameworkCore
public static AsbCloudDbContext MakeTestContext()
public static AsbCloudDbContext MakeRealTestContext()
{
var options = new DbContextOptionsBuilder<AsbCloudDbContext>()
//.UseInMemoryDatabase(System.Guid.NewGuid().ToString())
@ -22,5 +23,38 @@ namespace AsbCloudWebApi.Tests
context.Database.EnsureCreated();
return context;
}
public static Mock<IAsbCloudDbContext> AddDbSetMock<T>(this Mock<IAsbCloudDbContext> contextMock, IEnumerable<T> dbSetData)
where T : class
{
var dbSetMock = MakeDbSetMock(dbSetData);
contextMock.Setup(o => o.Set<T>())
.Returns(() => dbSetMock.Object);
return contextMock;
}
public static Mock<DbSet<T>> MakeDbSetMock<T>()
where T : class
{
var dbSetData = new List<T>();
return MakeDbSetMock(dbSetData);
}
public static Mock<DbSet<T>> MakeDbSetMock<T>(IEnumerable<T> dbSetData)
where T : class
{
var dbSetDataQueriable = dbSetData
.ToList()
.AsQueryable();
Mock<DbSet<T>> dbSetMock = new();
dbSetMock.As<IQueryable<T>>().Setup(o => o.Provider).Returns(() => dbSetDataQueriable.Provider);
dbSetMock.As<IQueryable<T>>().Setup(o => o.Expression).Returns(() => dbSetDataQueriable.Expression);
dbSetMock.As<IQueryable<T>>().Setup(o => o.ElementType).Returns(() => dbSetDataQueriable.ElementType);
dbSetMock.As<IQueryable<T>>().Setup(o => o.GetEnumerator()).Returns(() => dbSetDataQueriable.GetEnumerator());
return dbSetMock;
}
}
}

View File

@ -37,7 +37,7 @@ namespace AsbCloudWebApi.Controllers
/// <param name="idWell">id скважины</param>
/// <param name="idCategory">id категории файла</param>
/// <param name="files">Коллекция файлов</param>
/// <param name="userService">dependency</param>
/// <param name="userRepository">dependency</param>
/// <param name="token"> Токен отмены задачи </param>
/// <returns></returns>
[HttpPost]
@ -100,7 +100,6 @@ namespace AsbCloudWebApi.Controllers
/// <summary>
/// Возвращает файл с диска на сервере
/// </summary>
/// <param name="idWell">id скважины</param>
/// <param name="idFile">id запрашиваемого файла</param>
/// <param name="token"> Токен отмены задачи </param>
/// <returns>Запрашиваемый файл</returns>
@ -108,7 +107,7 @@ namespace AsbCloudWebApi.Controllers
[Route("{idFile}")]
[Permission]
[ProducesResponseType(typeof(PhysicalFileResult), (int)System.Net.HttpStatusCode.OK)]
public async Task<IActionResult> GetFileAsync([FromRoute] int idWell,
public async Task<IActionResult> GetFileAsync(
int idFile, CancellationToken token = default)
{
int? idCompany = User.GetCompanyId();
@ -135,7 +134,7 @@ namespace AsbCloudWebApi.Controllers
/// </summary>
/// <param name="idWell">id скважины</param>
/// <param name="idFile">id запрашиваемого файла</param>
/// <param name="userService">dependency</param>
/// <param name="userRepository">dependency</param>
/// <param name="token">Токен отмены задачи </param>
/// <returns></returns>
[HttpDelete("{idFile}")]

View File

@ -86,7 +86,7 @@ namespace AsbCloudWebApi.Controllers.SAUB
if (!isCompanyOwnsWell)
return Forbid();
var content = await telemetryDataService.GetAsync(idWell, begin,
var content = await telemetryDataService.GetOrDefaultAsync(idWell, begin,
intervalSec, approxPointsCount, token).ConfigureAwait(false);
return Ok(content);

View File

@ -89,7 +89,7 @@ namespace AsbCloudWebApi.Controllers.SAUB
if (!isCompanyOwnsWell)
return Forbid();
int? idTelemetry = telemetryService.GetIdTelemetryByIdWell(idWell);
int? idTelemetry = telemetryService.GetOrDefaultIdTelemetryByIdWell(idWell);
if (idTelemetry is null)
return NoContent();

View File

@ -72,7 +72,7 @@ namespace AsbCloudWebApi.Controllers.WITS
[FromServices] IWitsRecordRepository<TDto> witsRecordRepository,
CancellationToken token)
{
var idTelemetry = telemetryService.GetIdTelemetryByIdWell(idWell);
var idTelemetry = telemetryService.GetOrDefaultIdTelemetryByIdWell(idWell);
if (idTelemetry is null)
return NoContent();
var dtos = await witsRecordRepository.GetLastAsync((int)idTelemetry, token);
@ -96,7 +96,7 @@ namespace AsbCloudWebApi.Controllers.WITS
[FromServices] IWitsRecordRepository<TDto> witsRecordRepository,
CancellationToken token)
{
var idTelemetry = telemetryService.GetIdTelemetryByIdWell(idWell);
var idTelemetry = telemetryService.GetOrDefaultIdTelemetryByIdWell(idWell);
if (idTelemetry is null)
return NoContent();
var dtos = await witsRecordRepository.GetAsync((int)idTelemetry, begin, end, token);
@ -118,7 +118,7 @@ namespace AsbCloudWebApi.Controllers.WITS
[FromServices] IWitsRecordRepository<TDto> witsRecordRepository,
CancellationToken token)
{
var idTelemetry = telemetryService.GetIdTelemetryByIdWell(idWell);
var idTelemetry = telemetryService.GetOrDefaultIdTelemetryByIdWell(idWell);
if (idTelemetry is null)
return NoContent();
var dtos = await witsRecordRepository.GetStatAsync((int)idTelemetry, token);

View File

@ -14,25 +14,26 @@ namespace AsbCloudWebApi.Middlewares
/// </summary>
class UserConnectionsLimitMiddlware
{
private readonly RequestDelegate next;
private readonly int parallelRequestsToController;
private readonly byte[] body;
private readonly RequestDelegate next;
private readonly byte[] responseBody;
private readonly ConcurrentDictionary<int, ConcurrentDictionary<string, int>> stat = new ();
private readonly IEnumerable<string>? controllerNames;
public UserConnectionsLimitMiddlware(RequestDelegate next, IConfiguration configuration)
{
const int parallelRequestsToControllerDefault = 8;
this.next = next;
var parallelRequestsToController = configuration.GetSection("userLimits")?.GetValue<int>("parallelRequestsToController") ?? 5;
var parallelRequestsToController = configuration.GetSection("userLimits")?.GetValue<int>("parallelRequestsToController") ?? parallelRequestsToControllerDefault;
this.parallelRequestsToController = parallelRequestsToController > 0
? parallelRequestsToController
: 5;
: parallelRequestsToControllerDefault;
controllerNames = configuration.GetSection("userLimits")?.GetValue<IEnumerable<string>>("controllerNames");
var bodyText = $"<html><head><title>Too Many Requests</title></head><body><h1>Too Many Requests</h1><p>I only allow {parallelRequestsToController} parallel requests per user. Try again soon.</p></body></html>";
body = System.Text.Encoding.UTF8.GetBytes(bodyText);
responseBody = System.Text.Encoding.UTF8.GetBytes(bodyText);
}
public async Task InvokeAsync(HttpContext context, int idUser, string controllerName)
@ -44,11 +45,13 @@ namespace AsbCloudWebApi.Middlewares
}
var userStat = stat.GetOrAdd(idUser, idUser => new());
var count = userStat.AddOrUpdate(controllerName, 1, (key, value) => value + 1);
if(count < parallelRequestsToController)
var count = userStat.AddOrUpdate(controllerName, 0, (k, v) => v);
if(count + 1 < parallelRequestsToController)
{
try
{
userStat[controllerName]++;
await next(context);
}
finally
@ -60,10 +63,9 @@ namespace AsbCloudWebApi.Middlewares
{
context.Response.Clear();
context.Response.StatusCode = (int)System.Net.HttpStatusCode.TooManyRequests;
context.Response.Headers.RetryAfter = "1000";
context.Response.Headers.ContentType = "text/html";
await context.Response.BodyWriter.WriteAsync(body);
await context.Response.BodyWriter.WriteAsync(responseBody);
}
}
}

View File

@ -1 +1,17 @@
<!doctype html><html lang="ru"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="white"/><meta name="theme-color" media="(prefers-color-scheme: light)" content="white"/><meta name="theme-color" media="(prefers-color-scheme: dark)" content="black"/><meta name="description" content="Онлайн мониторинг процесса бурения в реальном времени в офисе заказчика"/><title>DDrilling</title><script defer="defer" src="/vendors.1920da1d.js"></script><script defer="defer" src="/main.101601f2.js"></script></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="white" />
<meta name="theme-color" media="(prefers-color-scheme: light)" content="white" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="black" />
<meta name="description" content="Онлайн мониторинг процесса бурения в реальном времени в офисе заказчика" />
<title>DDrilling</title>
<script defer src="/runtime~main.5cad8e9a.js"></script><script defer src="/vendors.0c60e3f8.js"></script><script defer src="/main.3e1e062c.js"></script><link href="/main.e200693d.css" rel="stylesheet"></head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

View File

@ -2,7 +2,9 @@
using AsbCloudApp.Data.DailyReport;
using AsbCloudDb;
using AsbCloudDb.Model;
using AsbCloudInfrastructure;
using AsbCloudInfrastructure.Services.DailyReport;
using AsbCloudInfrastructure.Services.SAUB;
using ClosedXML.Excel;
using DocumentFormat.OpenXml.Wordprocessing;
using Microsoft.EntityFrameworkCore;
@ -17,7 +19,6 @@ using System.Threading.Tasks;
namespace ConsoleApp1
{
class Program
{
private static AsbCloudDbContext db = ServiceFactory.Context;
@ -25,14 +26,13 @@ namespace ConsoleApp1
// use ServiceFactory to make services
static void Main(/*string[] args*/)
{
var h = new Hashtable();
h.Add("name", 1);
h.Add("name2", "66");
var v = h["v"];
DependencyInjection.MapsterSetup();
var sw = System.Diagnostics.Stopwatch.StartNew();
var s = System.Text.Json.JsonSerializer.Serialize(h);
Console.WriteLine($"total time: ms");
sw.Stop();
Console.WriteLine($"total time: {sw.ElapsedMilliseconds} ms");
Console.ReadLine();
}
}