forked from ddrilling/AsbCloudServer
Merge branch 'dev' into feature/7887519
This commit is contained in:
commit
49cc9c2342
@ -18,10 +18,6 @@
|
|||||||
<EditorConfigFiles Remove="D:\Source\AsbCloudApp\Services\.editorconfig" />
|
<EditorConfigFiles Remove="D:\Source\AsbCloudApp\Services\.editorconfig" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Include="D:\Source\AsbCloudApp\Services\.editorconfig" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\AsbCloudDb\AsbCloudDb.csproj" />
|
<ProjectReference Include="..\AsbCloudDb\AsbCloudDb.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
198
AsbCloudApp/CyclycArray.cs
Normal file
198
AsbCloudApp/CyclycArray.cs
Normal 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
|
@ -10,7 +10,7 @@ namespace AsbCloudApp.Data.Subsystems
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Активная скважина
|
/// Активная скважина
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public WellDto Well { get; set; }
|
public WellDto Well { get; set; } = null!;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Наработки подсистемы АКБ
|
/// Наработки подсистемы АКБ
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -51,7 +51,6 @@ namespace AsbCloudApp.Repositories
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Вывод списка всех файлов из базы, для которых нет файла на диске
|
/// Вывод списка всех файлов из базы, для которых нет файла на диске
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="idWell"></param>
|
|
||||||
/// <param name="files"></param>
|
/// <param name="files"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
IEnumerable<FileInfoDto> GetListFilesNotDisc(IEnumerable<FileInfoDto> files);
|
IEnumerable<FileInfoDto> GetListFilesNotDisc(IEnumerable<FileInfoDto> files);
|
||||||
|
@ -165,15 +165,6 @@ namespace AsbCloudApp.Services
|
|||||||
public async Task<IEnumerable<FileInfoDto>> GetInfoByIdsAsync(IEnumerable<int> idsFile, CancellationToken token)
|
public async Task<IEnumerable<FileInfoDto>> GetInfoByIdsAsync(IEnumerable<int> idsFile, CancellationToken token)
|
||||||
{
|
{
|
||||||
var result = await fileRepository.GetInfoByIdsAsync(idsFile, token).ConfigureAwait(false);
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace AsbCloudApp.Services
|
namespace AsbCloudApp.Services
|
||||||
{
|
{
|
||||||
|
#nullable enable
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Сервис авторизации
|
/// Сервис авторизации
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -33,13 +34,14 @@ namespace AsbCloudApp.Services
|
|||||||
/// <param name="password"></param>
|
/// <param name="password"></param>
|
||||||
/// <param name="token">токен отмены задачи</param>
|
/// <param name="token">токен отмены задачи</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<UserTokenDto> LoginAsync(string login,
|
Task<UserTokenDto?> LoginAsync(string login,
|
||||||
string password, CancellationToken token = default);
|
string password, CancellationToken token = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Обновление токена авторизации
|
/// Обновление токена авторизации
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="user"></param>
|
/// <param name="identity"></param>
|
||||||
|
/// <param name="token"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<UserTokenDto?> RefreshAsync(ClaimsPrincipal identity,
|
Task<UserTokenDto?> RefreshAsync(ClaimsPrincipal identity,
|
||||||
CancellationToken token);
|
CancellationToken token);
|
||||||
@ -51,4 +53,5 @@ namespace AsbCloudApp.Services
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
int Register(UserRegistrationDto userDto);
|
int Register(UserRegistrationDto userDto);
|
||||||
}
|
}
|
||||||
|
#nullable disable
|
||||||
}
|
}
|
@ -10,6 +10,12 @@ namespace AsbCloudApp.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IFileCategoryService
|
public interface IFileCategoryService
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Получить категории файлов
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id"></param>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns></returns>
|
||||||
Task<FileCategoryDto> GetOrDefaultAsync(int id, CancellationToken token);
|
Task<FileCategoryDto> GetOrDefaultAsync(int id, CancellationToken token);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -3,7 +3,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
#nullable enable
|
||||||
namespace AsbCloudApp.Services
|
namespace AsbCloudApp.Services
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -22,7 +22,7 @@ namespace AsbCloudApp.Services
|
|||||||
/// <param name="approxPointsCount">кол-во элементов до которых эти данные прореживаются</param>
|
/// <param name="approxPointsCount">кол-во элементов до которых эти данные прореживаются</param>
|
||||||
/// <param name="token"></param>
|
/// <param name="token"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<IEnumerable<TDto>> GetAsync(int idWell,
|
Task<IEnumerable<TDto>?> GetOrDefaultAsync(int idWell,
|
||||||
DateTime dateBegin = default, double intervalSec = 600d,
|
DateTime dateBegin = default, double intervalSec = 600d,
|
||||||
int approxPointsCount = 1024, CancellationToken token = default);
|
int approxPointsCount = 1024, CancellationToken token = default);
|
||||||
|
|
||||||
@ -36,3 +36,4 @@ namespace AsbCloudApp.Services
|
|||||||
Task<int> UpdateDataAsync(string uid, IEnumerable<TDto> dtos, CancellationToken token = default);
|
Task<int> UpdateDataAsync(string uid, IEnumerable<TDto> dtos, CancellationToken token = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#nullable disable
|
@ -55,7 +55,7 @@ namespace AsbCloudApp.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="idWell"></param>
|
/// <param name="idWell"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
int? GetIdTelemetryByIdWell(int idWell);
|
int? GetOrDefaultIdTelemetryByIdWell(int idWell);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// получить диапазон дат за которые есть данные
|
/// получить диапазон дат за которые есть данные
|
||||||
|
@ -6,8 +6,19 @@ using System.Threading.Tasks;
|
|||||||
namespace AsbCloudApp.Services.Subsystems
|
namespace AsbCloudApp.Services.Subsystems
|
||||||
{
|
{
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
// TODO: move this to repositories
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// репозиторий получения подсистем
|
||||||
|
/// </summary>
|
||||||
public interface ISubsystemService
|
public interface ISubsystemService
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// получение списка подсистем. Если скважина указана, то получим только использованные в скважине подсистемы.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idWell"></param>
|
||||||
|
/// <param name="token"></param>
|
||||||
|
/// <returns></returns>
|
||||||
Task<IEnumerable<SubsystemDto>?> GetSubsystemAsync(int? idWell, CancellationToken token);
|
Task<IEnumerable<SubsystemDto>?> GetSubsystemAsync(int? idWell, CancellationToken token);
|
||||||
}
|
}
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
@ -61,14 +61,6 @@ namespace AsbCloudDb.Model
|
|||||||
public DbSet<WITS.Record60> Record60 => Set<WITS.Record60>();
|
public DbSet<WITS.Record60> Record60 => Set<WITS.Record60>();
|
||||||
public DbSet<WITS.Record61> Record61 => Set<WITS.Record61>();
|
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()
|
public AsbCloudDbContext() : base()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@ -369,10 +361,5 @@ namespace AsbCloudDb.Model
|
|||||||
var sql = $"REFRESH MATERIALIZED VIEW {materializedViewName};";
|
var sql = $"REFRESH MATERIALIZED VIEW {materializedViewName};";
|
||||||
return Database.ExecuteSqlRawAsync(sql, token);
|
return Database.ExecuteSqlRawAsync(sql, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<int> RefreshMaterializedViewAsync<TEntity>(string? mwName = null, CancellationToken token = default) where TEntity : class
|
|
||||||
{
|
|
||||||
throw new System.NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,58 +5,57 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// КНБК описание
|
/// КНБК описание
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string BHADescription { get; set; }
|
public string BHADescription { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Бурение с наращиваниями в инт. 2195-2763м. Время начала
|
/// Бурение с наращиваниями в инт. 2195-2763м. Время начала
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ExtensionDrillingOneBegin { get; set; }
|
public string ExtensionDrillingOneBegin { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Бурение с наращиваниями в инт. 2195-2763м. Время окончания
|
/// Бурение с наращиваниями в инт. 2195-2763м. Время окончания
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ExtensionDrillingOneFinish { get; set; }
|
public string ExtensionDrillingOneFinish { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Промывка. Время начала
|
/// Промывка. Время начала
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string SluiceBegin { get; set; }
|
public string SluiceBegin { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Промывка. Время окончания
|
/// Промывка. Время окончания
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string SluiceFinish { get; set; }
|
public string SluiceFinish { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Подьем КНБК. Время начала
|
/// Подьем КНБК. Время начала
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ClimbBegin { get; set; }
|
public string ClimbBegin { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Подьем КНБК. Время окончания
|
/// Подьем КНБК. Время окончания
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ClimbFinish { get; set; }
|
public string ClimbFinish { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Спуск КНБК. Время начала
|
/// Спуск КНБК. Время начала
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string DescentBegin { get; set; }
|
public string DescentBegin { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Спуск КНБК. Время окончания
|
/// Спуск КНБК. Время окончания
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string DescentFinish { get; set; }
|
public string DescentFinish { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Бурение с наращиваниями в инт. 2763-2850м. Время начала
|
/// Бурение с наращиваниями в инт. 2763-2850м. Время начала
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ExtensionDrillingTwoBegin { get; set; }
|
public string ExtensionDrillingTwoBegin { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Бурение с наращиваниями в инт. 2763-2850м. Время окончания
|
/// Бурение с наращиваниями в инт. 2763-2850м. Время окончания
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ExtensionDrillingTwoFinish { get; set; }
|
public string ExtensionDrillingTwoFinish { get; set; } = string.Empty;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,22 +6,22 @@ namespace AsbCloudDb.Model.DailyReport
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// название скважины
|
/// название скважины
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string WellName { get; set; }
|
public string WellName { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// название куста
|
/// название куста
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ClusterName { get; set; }
|
public string ClusterName { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// заказчик
|
/// заказчик
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Customer { get; set; }
|
public string Customer { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// подрядчик
|
/// подрядчик
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Contractor { get; set; }
|
public string Contractor { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// дата рапорта
|
/// дата рапорта
|
||||||
@ -61,12 +61,12 @@ namespace AsbCloudDb.Model.DailyReport
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ФИО бурильщиков
|
/// ФИО бурильщиков
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string FirstDriller { get; set; }
|
public string FirstDriller { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ФИО бурильщиков
|
/// ФИО бурильщиков
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string SecondDriller { get; set; }
|
public string SecondDriller { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Время работы АПД
|
/// Время работы АПД
|
||||||
|
@ -72,32 +72,32 @@ namespace AsbCloudDb.Model.DailyReport
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// указываются все причины, которые влияют на снижение МСП.
|
/// указываются все причины, которые влияют на снижение МСП.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string DeclinesReasonsROP { get; set; }
|
public string DeclinesReasonsROP { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Увеличение мех скорости за секцию %
|
/// Увеличение мех скорости за секцию %
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string IncreaseSpeedSection { get; set; }
|
public string IncreaseSpeedSection { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Увеличение мех скорости за сутки %
|
/// Увеличение мех скорости за сутки %
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string IncreaseSpeedDay { get; set; }
|
public string IncreaseSpeedDay { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Сокращение времени бурения за секцию, ч
|
/// Сокращение времени бурения за секцию, ч
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ReductionTimeDrilling { get; set; }
|
public string ReductionTimeDrilling { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ротор/Слайд %
|
/// Ротор/Слайд %
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string RotorSlidePercent { get; set; }
|
public string RotorSlidePercent { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// МСП
|
/// МСП
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string MspSection { get; set; }
|
public string MspSection { get; set; } = string.Empty;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,12 +5,12 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ФИО Мастера буровой
|
/// ФИО Мастера буровой
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string DrillingMaster { get; set; }
|
public string DrillingMaster { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ФИО супервайзера
|
/// ФИО супервайзера
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Supervisor { get; set; }
|
public string Supervisor { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,92 +5,92 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Бурение
|
/// Бурение
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Drilling { get; set; }
|
public string Drilling { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Промывка
|
/// Промывка
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Flushing { get; set; }
|
public string Flushing { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Наращивание
|
/// Наращивание
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Building { get; set; }
|
public string Building { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Проработка
|
/// Проработка
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Elaboration { get; set; }
|
public string Elaboration { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Расширка
|
/// Расширка
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Extension { get; set; }
|
public string Extension { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ремонт
|
/// Ремонт
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Repair { get; set; }
|
public string Repair { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// КНБК
|
/// КНБК
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Knbk { get; set; }
|
public string Knbk { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// СПО
|
/// СПО
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Spo { get; set; }
|
public string Spo { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ПЗР
|
/// ПЗР
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Pzr { get; set; }
|
public string Pzr { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ПВО
|
/// ПВО
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Pvo { get; set; }
|
public string Pvo { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ПГР
|
/// ПГР
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Pgr { get; set; }
|
public string Pgr { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ГИС
|
/// ГИС
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Gis { get; set; }
|
public string Gis { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ОЗЦ
|
/// ОЗЦ
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Ozc { get; set; }
|
public string Ozc { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Тех. работы
|
/// Тех. работы
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string EngineeringWorks { get; set; }
|
public string EngineeringWorks { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Снятие замера
|
/// Снятие замера
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string TakingMeasure { get; set; }
|
public string TakingMeasure { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Цементирование
|
/// Цементирование
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Cementing { get; set; }
|
public string Cementing { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Простой
|
/// Простой
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Simple { get; set; }
|
public string Simple { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// НПВ
|
/// НПВ
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Npv { get; set; }
|
public string Npv { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ using AsbCloudInfrastructure.Services.SAUB;
|
|||||||
using AsbCloudInfrastructure.Services.Subsystems;
|
using AsbCloudInfrastructure.Services.Subsystems;
|
||||||
using AsbCloudInfrastructure.Services.WellOperationService;
|
using AsbCloudInfrastructure.Services.WellOperationService;
|
||||||
using AsbCloudInfrastructure.Validators;
|
using AsbCloudInfrastructure.Validators;
|
||||||
|
using DocumentFormat.OpenXml.Spreadsheet;
|
||||||
using FluentValidation.AspNetCore;
|
using FluentValidation.AspNetCore;
|
||||||
using Mapster;
|
using Mapster;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@ -90,7 +91,8 @@ namespace AsbCloudInfrastructure
|
|||||||
services.AddDbContext<AsbCloudDbContext>(options =>
|
services.AddDbContext<AsbCloudDbContext>(options =>
|
||||||
options.UseNpgsql(configuration.GetConnectionString(connectionStringName)));
|
options.UseNpgsql(configuration.GetConnectionString(connectionStringName)));
|
||||||
|
|
||||||
services.AddFluentValidation();
|
// TODO: переместить FluentValidation в описание моделей
|
||||||
|
services.AddFluentValidationClientsideAdapters();
|
||||||
|
|
||||||
services.AddScoped<IAsbCloudDbContext>(provider => provider.GetService<AsbCloudDbContext>());
|
services.AddScoped<IAsbCloudDbContext>(provider => provider.GetService<AsbCloudDbContext>());
|
||||||
services.AddScoped<IEmailService, EmailService>();
|
services.AddScoped<IEmailService, EmailService>();
|
||||||
@ -101,6 +103,8 @@ namespace AsbCloudInfrastructure
|
|||||||
services.AddSingleton(new WitsInfoService());
|
services.AddSingleton(new WitsInfoService());
|
||||||
services.AddSingleton(new CacheDb());
|
services.AddSingleton(new CacheDb());
|
||||||
services.AddSingleton(new InstantDataRepository());
|
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<ITelemetryTracker, TelemetryTracker>();
|
||||||
services.AddSingleton<IRequerstTrackerService, RequestTrackerService>();
|
services.AddSingleton<IRequerstTrackerService, RequestTrackerService>();
|
||||||
services.AddSingleton<IBackgroundWorkerService, BackgroundWorkerService>();
|
services.AddSingleton<IBackgroundWorkerService, BackgroundWorkerService>();
|
||||||
|
@ -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<<typeparamref name="TKey"/>, <typeparamref name="TEntity"/>>. С тегом 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<<typeparamref name="TKey"/>, <typeparamref name="TEntity"/>>. С тегом 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<<typeparamref name="TKey"/>, <typeparamref name="TEntity"/>>.
|
|
||||||
/// </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<<typeparamref name="TKey"/>, <typeparamref name="TModel"/>>.
|
|
||||||
/// </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<<typeparamref name="TKey"/>, <typeparamref name="TEntity"/>>. С тегом 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<<typeparamref name="TKey"/>, <typeparamref name="TEntity"/>>. С тегом 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<<typeparamref name="TKey"/>, <typeparamref name="TEntity"/>>.
|
|
||||||
/// </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<<typeparamref name="TKey"/>, <typeparamref name="TModel"/>>.
|
|
||||||
/// </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
|
|
||||||
}
|
|
@ -349,7 +349,7 @@ namespace AsbCloudInfrastructure.EfCache
|
|||||||
return cache.GetData(convert);
|
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
|
where TEntity : class
|
||||||
{
|
{
|
||||||
var tag = typeof(TEntity).Name;
|
var tag = typeof(TEntity).Name;
|
||||||
@ -365,7 +365,7 @@ namespace AsbCloudInfrastructure.EfCache
|
|||||||
/// <param name="obsolescence">Период устаревания данных</param>
|
/// <param name="obsolescence">Период устаревания данных</param>
|
||||||
/// <param name="token"></param>
|
/// <param name="token"></param>
|
||||||
/// <returns></returns>
|
/// <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
|
where TEntity : class
|
||||||
{
|
{
|
||||||
async Task<object[]> factory(CancellationToken token)
|
async Task<object[]> factory(CancellationToken token)
|
||||||
@ -386,7 +386,7 @@ namespace AsbCloudInfrastructure.EfCache
|
|||||||
/// <param name="convert">Преобразование данных БД в DTO</param>
|
/// <param name="convert">Преобразование данных БД в DTO</param>
|
||||||
/// <param name="token"></param>
|
/// <param name="token"></param>
|
||||||
/// <returns></returns>
|
/// <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
|
where TEntity : class
|
||||||
{
|
{
|
||||||
async Task<object[]> factory(CancellationToken token)
|
async Task<object[]> factory(CancellationToken token)
|
||||||
|
@ -54,7 +54,7 @@ namespace AsbCloudInfrastructure.Repository
|
|||||||
public override async Task<IEnumerable<TDto>> GetAllAsync(CancellationToken token)
|
public override async Task<IEnumerable<TDto>> GetAllAsync(CancellationToken token)
|
||||||
{
|
{
|
||||||
var cache = await GetCacheAsync(token);
|
var cache = await GetCacheAsync(token);
|
||||||
return cache.Values;
|
return cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -65,14 +65,14 @@ namespace AsbCloudInfrastructure.Repository
|
|||||||
public override TDto? GetOrDefault(int id)
|
public override TDto? GetOrDefault(int id)
|
||||||
{
|
{
|
||||||
var cache = GetCache();
|
var cache = GetCache();
|
||||||
return cache.GetValueOrDefault(id);
|
return cache.FirstOrDefault(d => d.Id == id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override async Task<TDto?> GetOrDefaultAsync(int id, CancellationToken token)
|
public override async Task<TDto?> GetOrDefaultAsync(int id, CancellationToken token)
|
||||||
{
|
{
|
||||||
var cache = await GetCacheAsync(token);
|
var cache = await GetCacheAsync(token);
|
||||||
return cache.GetValueOrDefault(id);
|
return cache.FirstOrDefault(d => d.Id == id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@ -102,17 +102,17 @@ namespace AsbCloudInfrastructure.Repository
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual Task<Dictionary<int, TDto>> GetCacheAsync(CancellationToken token)
|
protected virtual Task<IEnumerable<TDto>> GetCacheAsync(CancellationToken token)
|
||||||
=> GetQuery()
|
=> GetQuery()
|
||||||
.FromCacheDictionaryAsync(CacheTag, CacheOlescence, KeySelector, Convert, token);
|
.FromCacheAsync(CacheTag, CacheOlescence, Convert, token);
|
||||||
|
|
||||||
|
|
||||||
protected virtual Dictionary<int, TDto> GetCache()
|
protected virtual IEnumerable<TDto> GetCache()
|
||||||
=> GetQuery()
|
=> GetQuery()
|
||||||
.FromCacheDictionary(CacheTag, CacheOlescence, KeySelector, Convert);
|
.FromCache(CacheTag, CacheOlescence, Convert);
|
||||||
|
|
||||||
protected virtual void DropCache()
|
protected virtual void DropCache()
|
||||||
=> dbSet.DropCacheDictionary(CacheTag);
|
=> dbSet.DropCache(CacheTag);
|
||||||
}
|
}
|
||||||
#nullable disable
|
#nullable disable
|
||||||
}
|
}
|
@ -23,25 +23,25 @@ namespace AsbCloudInfrastructure.Repository
|
|||||||
public CrudWellRelatedCacheServiceBase(IAsbCloudDbContext context, Func<DbSet<TEntity>, IQueryable<TEntity>> makeQuery)
|
public CrudWellRelatedCacheServiceBase(IAsbCloudDbContext context, Func<DbSet<TEntity>, IQueryable<TEntity>> makeQuery)
|
||||||
: base(context, 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 cache = await GetCacheAsync(token);
|
||||||
|
|
||||||
var dtos = cache.Values
|
var dtos = cache
|
||||||
.Where(e => e.IdWell == idWell)
|
.Where(e => e.IdWell == idWell)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
return dtos;
|
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())
|
if (!idsWells.Any())
|
||||||
return Enumerable.Empty<TDto>();
|
return Enumerable.Empty<TDto>();
|
||||||
|
|
||||||
var cache = await GetCacheAsync(token);
|
var cache = await GetCacheAsync(token);
|
||||||
|
|
||||||
var dtos = cache.Values
|
var dtos = cache
|
||||||
.Where(e => idsWells.Contains(e.IdWell))
|
.Where(e => idsWells.Contains(e.IdWell))
|
||||||
.ToList();
|
.ToList();
|
||||||
return dtos;
|
return dtos;
|
||||||
|
@ -23,7 +23,7 @@ namespace AsbCloudInfrastructure.Repository
|
|||||||
public CrudWellRelatedServiceBase(IAsbCloudDbContext context, Func<DbSet<TEntity>, IQueryable<TEntity>> makeQuery)
|
public CrudWellRelatedServiceBase(IAsbCloudDbContext context, Func<DbSet<TEntity>, IQueryable<TEntity>> makeQuery)
|
||||||
: base(context, 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()
|
var entities = await GetQuery()
|
||||||
.Where(e => e.IdWell == idWell)
|
.Where(e => e.IdWell == idWell)
|
||||||
@ -32,7 +32,7 @@ namespace AsbCloudInfrastructure.Repository
|
|||||||
return dtos;
|
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())
|
if (!idsWells.Any())
|
||||||
return Enumerable.Empty<TDto>();
|
return Enumerable.Empty<TDto>();
|
||||||
|
@ -12,6 +12,7 @@ using AsbCloudApp.Data.DailyReport;
|
|||||||
using AsbCloudApp.Requests;
|
using AsbCloudApp.Requests;
|
||||||
using AsbCloudInfrastructure.Services.DetectOperations;
|
using AsbCloudInfrastructure.Services.DetectOperations;
|
||||||
using AsbCloudApp.Data.DetectedOperation;
|
using AsbCloudApp.Data.DetectedOperation;
|
||||||
|
using AsbCloudApp.Exceptions;
|
||||||
|
|
||||||
namespace AsbCloudInfrastructure.Services.DailyReport
|
namespace AsbCloudInfrastructure.Services.DailyReport
|
||||||
{
|
{
|
||||||
@ -34,7 +35,8 @@ namespace AsbCloudInfrastructure.Services.DailyReport
|
|||||||
{
|
{
|
||||||
var well = wellService.GetOrDefault(idWell);
|
var well = wellService.GetOrDefault(idWell);
|
||||||
if (well is null || well.Timezone is null)
|
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);
|
var query = db.DailyReports.Where(r => r.IdWell == idWell);
|
||||||
|
|
||||||
DateTimeOffset ExtractDate(DateTime dateTime)
|
DateTimeOffset ExtractDate(DateTime dateTime)
|
||||||
@ -65,9 +67,8 @@ namespace AsbCloudInfrastructure.Services.DailyReport
|
|||||||
public async Task<DailyReportDto> GetOrGenerateAsync(int idWell, DateTime date, CancellationToken token)
|
public async Task<DailyReportDto> GetOrGenerateAsync(int idWell, DateTime date, CancellationToken token)
|
||||||
{
|
{
|
||||||
var dateOnly = DateTime.SpecifyKind(date.Date, DateTimeKind.Utc);
|
var dateOnly = DateTime.SpecifyKind(date.Date, DateTimeKind.Utc);
|
||||||
var dailyReportDto = await GetAsync(idWell, dateOnly, token);
|
var dailyReportDto = await GetOrDefaultAsync(idWell, dateOnly, token);
|
||||||
if (dailyReportDto is null)
|
dailyReportDto ??= await MakeDefaultDailyReportAsync(idWell, dateOnly, token);
|
||||||
dailyReportDto = await MakeDefaultDailyReportAsync(idWell, dateOnly, token);
|
|
||||||
return dailyReportDto;
|
return dailyReportDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,7 +106,7 @@ namespace AsbCloudInfrastructure.Services.DailyReport
|
|||||||
|
|
||||||
public async Task<Stream?> MakeReportAsync(int idWell, DateTime date, CancellationToken token = default)
|
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)
|
if (dailyReportDto is null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@ -113,7 +114,7 @@ namespace AsbCloudInfrastructure.Services.DailyReport
|
|||||||
return memoryStream;
|
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 dateOffset = date.Date;
|
||||||
var entity = await db.DailyReports
|
var entity = await db.DailyReports
|
||||||
|
@ -27,7 +27,7 @@ namespace AsbCloudInfrastructure.Services
|
|||||||
public async Task<DrillParamsDto?> GetDefaultDrillParamsAsync(int idWell,
|
public async Task<DrillParamsDto?> GetDefaultDrillParamsAsync(int idWell,
|
||||||
double startDepth, double endDepth, CancellationToken token = default)
|
double startDepth, double endDepth, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
var idTelemetry = telemetryService.GetIdTelemetryByIdWell(idWell);
|
var idTelemetry = telemetryService.GetOrDefaultIdTelemetryByIdWell(idWell);
|
||||||
|
|
||||||
if (idTelemetry is null)
|
if (idTelemetry is null)
|
||||||
return null;
|
return null;
|
||||||
|
@ -15,15 +15,15 @@ namespace AsbCloudInfrastructure.Services
|
|||||||
: base(context)
|
: base(context)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: Перенести в сервис "дело скважины"
|
//TODO: Перенести в сервис "дело скважины"
|
||||||
public async Task<IEnumerable<FileCategoryDto>> GetWellCaseCategoriesAsync(CancellationToken token)
|
public async Task<IEnumerable<FileCategoryDto>> GetWellCaseCategoriesAsync(CancellationToken token)
|
||||||
{
|
{
|
||||||
var cache = await GetCacheAsync(token)
|
var cache = await GetCacheAsync(token)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
var dtos = cache
|
var dtos = cache
|
||||||
.Where(kv => kv.Key >= 10000)
|
.Where(f => f.Id >= 10000)
|
||||||
.Where(kv => kv.Key <= 20000)
|
.Where(f => f.Id <= 20000);
|
||||||
.Select(kv => kv.Value);
|
|
||||||
|
|
||||||
return dtos;
|
return dtos;
|
||||||
}
|
}
|
||||||
|
@ -113,7 +113,7 @@ namespace AsbCloudInfrastructure.Services
|
|||||||
|
|
||||||
public DatesRangeDto GetDatesRangeOrDefault(int idWell)
|
public DatesRangeDto GetDatesRangeOrDefault(int idWell)
|
||||||
{
|
{
|
||||||
var idTelemetry = telemetryService.GetIdTelemetryByIdWell(idWell);
|
var idTelemetry = telemetryService.GetOrDefaultIdTelemetryByIdWell(idWell);
|
||||||
if (idTelemetry is null)
|
if (idTelemetry is null)
|
||||||
return null;
|
return null;
|
||||||
var range = telemetryService.GetDatesRange((int)idTelemetry);
|
var range = telemetryService.GetDatesRange((int)idTelemetry);
|
||||||
|
@ -34,7 +34,7 @@ namespace AsbCloudInfrastructure.Services.SAUB
|
|||||||
int take = 32,
|
int take = 32,
|
||||||
CancellationToken token = default)
|
CancellationToken token = default)
|
||||||
{
|
{
|
||||||
var idTelemetry = telemetryService.GetIdTelemetryByIdWell(idWell);
|
var idTelemetry = telemetryService.GetOrDefaultIdTelemetryByIdWell(idWell);
|
||||||
if (idTelemetry is null)
|
if (idTelemetry is null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
@ -10,26 +10,31 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
namespace AsbCloudInfrastructure.Services.SAUB
|
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 TDto : AsbCloudApp.Data.ITelemetryData
|
||||||
where TModel : class, ITelemetryData
|
where TEntity : class, ITelemetryData
|
||||||
{
|
{
|
||||||
protected readonly IAsbCloudDbContext db;
|
protected readonly IAsbCloudDbContext db;
|
||||||
private readonly ITelemetryService telemetryService;
|
private readonly ITelemetryService telemetryService;
|
||||||
protected readonly CacheTable<TelemetryUser> cacheTelemetryUsers;
|
protected readonly CacheTable<TelemetryUser> cacheTelemetryUsers;
|
||||||
|
private readonly TelemetryDataCache<TDto> telemetryDataCache;
|
||||||
|
|
||||||
public TelemetryDataBaseService(
|
public TelemetryDataBaseService(
|
||||||
IAsbCloudDbContext db,
|
IAsbCloudDbContext db,
|
||||||
ITelemetryService telemetryService,
|
ITelemetryService telemetryService,
|
||||||
|
TelemetryDataCache<TDto> telemetryDataCache,
|
||||||
CacheDb cacheDb)
|
CacheDb cacheDb)
|
||||||
{
|
{
|
||||||
this.db = db;
|
this.db = db;
|
||||||
this.telemetryService = telemetryService;
|
this.telemetryService = telemetryService;
|
||||||
cacheTelemetryUsers = cacheDb.GetCachedTable<TelemetryUser>((AsbCloudDbContext)db);
|
cacheTelemetryUsers = cacheDb.GetCachedTable<TelemetryUser>((AsbCloudDbContext)db);
|
||||||
|
this.telemetryDataCache = telemetryDataCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
public virtual async Task<int> UpdateDataAsync(string uid, IEnumerable<TDto> dtos, CancellationToken token = default)
|
public virtual async Task<int> UpdateDataAsync(string uid, IEnumerable<TDto> dtos, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
if (dtos == default || !dtos.Any())
|
if (dtos == default || !dtos.Any())
|
||||||
@ -53,6 +58,8 @@ namespace AsbCloudInfrastructure.Services.SAUB
|
|||||||
var idTelemetry = telemetryService.GetOrCreateTelemetryIdByUid(uid);
|
var idTelemetry = telemetryService.GetOrCreateTelemetryIdByUid(uid);
|
||||||
var timezone = telemetryService.GetTimezone(idTelemetry);
|
var timezone = telemetryService.GetTimezone(idTelemetry);
|
||||||
|
|
||||||
|
telemetryDataCache.AddRange(idTelemetry, dtos);
|
||||||
|
|
||||||
var entities = dtosList.Select(dto =>
|
var entities = dtosList.Select(dto =>
|
||||||
{
|
{
|
||||||
var entity = Convert(dto, timezone.Hours);
|
var entity = Convert(dto, timezone.Hours);
|
||||||
@ -63,7 +70,7 @@ namespace AsbCloudInfrastructure.Services.SAUB
|
|||||||
var entityMaxDate = entities.Max(e => e.DateTime);
|
var entityMaxDate = entities.Max(e => e.DateTime);
|
||||||
telemetryService.TelemetryTracker.SaveRequestDate(uid, entityMaxDate);
|
telemetryService.TelemetryTracker.SaveRequestDate(uid, entityMaxDate);
|
||||||
|
|
||||||
var dbset = db.Set<TModel>();
|
var dbset = db.Set<TEntity>();
|
||||||
var stopwatch = Stopwatch.StartNew();
|
var stopwatch = Stopwatch.StartNew();
|
||||||
try
|
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,
|
DateTime dateBegin = default, double intervalSec = 600d,
|
||||||
int approxPointsCount = 1024, CancellationToken token = default)
|
int approxPointsCount = 1024, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
var idTelemetry = telemetryService.GetIdTelemetryByIdWell(idWell) ?? -1;
|
var idTelemetry = telemetryService.GetOrDefaultIdTelemetryByIdWell(idWell) ?? -1;
|
||||||
if (idTelemetry == -1)
|
if (idTelemetry == -1)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@ -110,8 +120,12 @@ namespace AsbCloudInfrastructure.Services.SAUB
|
|||||||
if (dateBeginUtc == default)
|
if (dateBeginUtc == default)
|
||||||
dateBeginUtc = DateTime.UtcNow.AddSeconds(-intervalSec);
|
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 dateEnd = dateBeginUtc.AddSeconds(intervalSec);
|
||||||
var dbSet = db.Set<TModel>();
|
var dbSet = db.Set<TEntity>();
|
||||||
|
|
||||||
var query = dbSet
|
var query = dbSet
|
||||||
.Where(d => d.IdTelemetry == idTelemetry
|
.Where(d => d.IdTelemetry == idTelemetry
|
||||||
@ -144,9 +158,10 @@ namespace AsbCloudInfrastructure.Services.SAUB
|
|||||||
return dtos;
|
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
|
184
AsbCloudInfrastructure/Services/SAUB/TelemetryDataCache.cs
Normal file
184
AsbCloudInfrastructure/Services/SAUB/TelemetryDataCache.cs
Normal 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
|
@ -16,8 +16,9 @@ namespace AsbCloudInfrastructure.Services.SAUB
|
|||||||
public TelemetryDataSaubService(
|
public TelemetryDataSaubService(
|
||||||
IAsbCloudDbContext db,
|
IAsbCloudDbContext db,
|
||||||
ITelemetryService telemetryService,
|
ITelemetryService telemetryService,
|
||||||
|
TelemetryDataCache<TelemetryDataSaubDto> telemetryDataCache,
|
||||||
CacheDb cacheDb)
|
CacheDb cacheDb)
|
||||||
: base(db, telemetryService, cacheDb)
|
: base(db, telemetryService, telemetryDataCache, cacheDb)
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
public override TelemetryDataSaub Convert(TelemetryDataSaubDto src, double timezoneOffset)
|
public override TelemetryDataSaub Convert(TelemetryDataSaubDto src, double timezoneOffset)
|
||||||
|
@ -13,8 +13,9 @@ namespace AsbCloudInfrastructure.Services.SAUB
|
|||||||
public TelemetryDataSpinService(
|
public TelemetryDataSpinService(
|
||||||
IAsbCloudDbContext db,
|
IAsbCloudDbContext db,
|
||||||
ITelemetryService telemetryService,
|
ITelemetryService telemetryService,
|
||||||
|
TelemetryDataCache<TelemetryDataSpinDto> telemetryDataCache,
|
||||||
CacheDb cacheDb)
|
CacheDb cacheDb)
|
||||||
: base(db, telemetryService, cacheDb)
|
: base(db, telemetryService, telemetryDataCache, cacheDb)
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
public override TelemetryDataSpin Convert(TelemetryDataSpinDto src, double timezoneOffset)
|
public override TelemetryDataSpin Convert(TelemetryDataSpinDto src, double timezoneOffset)
|
||||||
|
@ -37,22 +37,22 @@ namespace AsbCloudInfrastructure.Services.SAUB
|
|||||||
this.timezoneService = timezoneService;
|
this.timezoneService = timezoneService;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Dictionary<int, Telemetry> GetTelemetryCache()
|
private IEnumerable<Telemetry> GetTelemetryCache()
|
||||||
{
|
{
|
||||||
var cache = db.Telemetries
|
var cache = db.Telemetries
|
||||||
.Include(t => t.Well)
|
.Include(t => t.Well)
|
||||||
.FromCacheDictionary(telemetryCacheTag, telemetryCacheObsolescence, t => t.Id);
|
.FromCache(telemetryCacheTag, telemetryCacheObsolescence);
|
||||||
return cache;
|
return cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DropTelemetryCache()
|
private void DropTelemetryCache()
|
||||||
{
|
{
|
||||||
db.Telemetries.DropCacheDictionary(telemetryCacheTag);
|
db.Telemetries.DropCache(telemetryCacheTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DateTime GetLastTelemetryDate(int idTelemetry)
|
public DateTime GetLastTelemetryDate(int idTelemetry)
|
||||||
{
|
{
|
||||||
var telemetry = GetTelemetryCache().GetValueOrDefault(idTelemetry);
|
var telemetry = GetTelemetryCache().FirstOrDefault(t => t.Id == idTelemetry);
|
||||||
|
|
||||||
if (telemetry is null)
|
if (telemetry is null)
|
||||||
throw new Exception($"Telemetry id:{idTelemetry} does not exist");
|
throw new Exception($"Telemetry id:{idTelemetry} does not exist");
|
||||||
@ -65,7 +65,7 @@ namespace AsbCloudInfrastructure.Services.SAUB
|
|||||||
|
|
||||||
public DatesRangeDto GetDatesRange(int idTelemetry)
|
public DatesRangeDto GetDatesRange(int idTelemetry)
|
||||||
{
|
{
|
||||||
var telemetry = GetTelemetryCache().GetValueOrDefault(idTelemetry);
|
var telemetry = GetTelemetryCache().FirstOrDefault(t => t.Id == idTelemetry);
|
||||||
if (telemetry is null)
|
if (telemetry is null)
|
||||||
throw new Exception($"Telemetry id:{idTelemetry} does not exist");
|
throw new Exception($"Telemetry id:{idTelemetry} does not exist");
|
||||||
|
|
||||||
@ -107,7 +107,7 @@ namespace AsbCloudInfrastructure.Services.SAUB
|
|||||||
|
|
||||||
public SimpleTimezoneDto GetTimezone(int idTelemetry)
|
public SimpleTimezoneDto GetTimezone(int idTelemetry)
|
||||||
{
|
{
|
||||||
var telemetry = GetTelemetryCache().GetValueOrDefault(idTelemetry);
|
var telemetry = GetTelemetryCache().FirstOrDefault(t => t.Id == idTelemetry);
|
||||||
|
|
||||||
if (telemetry is null)
|
if (telemetry is null)
|
||||||
throw new Exception($"Telemetry id: {idTelemetry} does not exist.");
|
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.");
|
throw new Exception($"Telemetry id: {idTelemetry} can't find timezone.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public int? GetIdTelemetryByIdWell(int idWell)
|
public int? GetOrDefaultIdTelemetryByIdWell(int idWell)
|
||||||
{
|
{
|
||||||
var telemetry = GetTelemetryCache()
|
var telemetry = GetTelemetryCache()
|
||||||
.FirstOrDefault(t => t.Value.Well?.Id == idWell).Value;
|
.FirstOrDefault(t => t.Well?.Id == idWell);
|
||||||
return telemetry?.Id;
|
return telemetry?.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,7 +156,7 @@ namespace AsbCloudInfrastructure.Services.SAUB
|
|||||||
|
|
||||||
private Telemetry? GetOrDefaultTelemetryByUid(string uid)
|
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;
|
return telemetry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,8 +81,7 @@ namespace AsbCloudInfrastructure.Services
|
|||||||
.Select(r => r.IdWell);
|
.Select(r => r.IdWell);
|
||||||
|
|
||||||
var wellsDtos = (await GetCacheAsync(token))
|
var wellsDtos = (await GetCacheAsync(token))
|
||||||
.Where(kv => wellsIds.Contains(kv.Key))
|
.Where(w => wellsIds.Contains(w.Id));
|
||||||
.Select(kv => kv.Value);
|
|
||||||
|
|
||||||
return wellsDtos.ToList();
|
return wellsDtos.ToList();
|
||||||
}
|
}
|
||||||
@ -95,7 +94,7 @@ namespace AsbCloudInfrastructure.Services
|
|||||||
if (dto.IdState is < 0 or > 2)
|
if (dto.IdState is < 0 or > 2)
|
||||||
throw new ArgumentInvalidException("Текущее состояние работы скважины указано неправильно.", nameof(dto));
|
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));
|
throw new ArgumentInvalidException($"Нельзя повторно добавить скважину с id: {dto.Id}", nameof(dto));
|
||||||
|
|
||||||
var entity = Convert(dto);
|
var entity = Convert(dto);
|
||||||
@ -194,7 +193,7 @@ namespace AsbCloudInfrastructure.Services
|
|||||||
|
|
||||||
var cache = await GetCacheAsync(token);
|
var cache = await GetCacheAsync(token);
|
||||||
|
|
||||||
var clusterWellsIds = cache.Values
|
var clusterWellsIds = cache
|
||||||
.Where((w) => w.IdCluster == well.IdCluster)
|
.Where((w) => w.IdCluster == well.IdCluster)
|
||||||
.Select(w => w.Id);
|
.Select(w => w.Id);
|
||||||
|
|
||||||
@ -247,7 +246,7 @@ namespace AsbCloudInfrastructure.Services
|
|||||||
public async Task EnshureTimezonesIsSetAsync(CancellationToken token)
|
public async Task EnshureTimezonesIsSetAsync(CancellationToken token)
|
||||||
{
|
{
|
||||||
var cache = await GetCacheAsync(token);
|
var cache = await GetCacheAsync(token);
|
||||||
if (!cache.Values.Any(w => w.Timezone is null))
|
if (!cache.Any(w => w.Timezone is null))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var defaultTimeZone = new SimpleTimezone
|
var defaultTimeZone = new SimpleTimezone
|
||||||
|
93
AsbCloudWebApi.Tests/CacheTests/TelemetryDataCacheTest.cs
Normal file
93
AsbCloudWebApi.Tests/CacheTests/TelemetryDataCacheTest.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -9,7 +9,42 @@ namespace AsbCloudWebApi.Tests.Middlware
|
|||||||
{
|
{
|
||||||
public class UserConnectionsLimitMiddlwareTest
|
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()
|
public UserConnectionsLimitMiddlwareTest()
|
||||||
{
|
{
|
||||||
@ -20,66 +55,53 @@ namespace AsbCloudWebApi.Tests.Middlware
|
|||||||
})
|
})
|
||||||
.Build();
|
.Build();
|
||||||
host.Start();
|
host.Start();
|
||||||
httpClient = new ();
|
|
||||||
httpClient.DefaultRequestHeaders.Authorization = new("Bearer", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiZGV2IiwiaWRDb21wYW55IjoiMSIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6InJvb3QiLCJuYmYiOjE2NjY1ODY2MjAsImV4cCI6MTY5ODE0NDIyMCwiaXNzIjoiYSIsImF1ZCI6ImEifQ.zqBdR4nYB87-Xyzv025waasN47i43c9FJ23RfzIvUsM");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Send_n_requests_and_get_blocked()
|
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;
|
var i = 0;
|
||||||
for (; i < 5; i++)
|
for (; i < iterations2Block; i++)
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
var well = wells[i];
|
var well = wells[i];
|
||||||
var url = MakeUrl(well.Item1, well.Item2, well.Item3);
|
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 response.Content.ReadAsStringAsync();
|
||||||
await Task.Delay(1000);
|
await Task.Delay(1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
var well = wells[i];
|
var well = wells[i];
|
||||||
var url = MakeUrl(well.Item1, well.Item2, well.Item3);
|
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);
|
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)
|
private static string MakeUrl(int idWell, DateTime dateBegin, DateTime dateEnd)
|
||||||
{
|
{
|
||||||
var interval = (dateEnd - dateBegin).TotalSeconds;
|
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}";
|
var url = $"http://127.0.0.1:5000/api/TelemetryDataSaub/{idWell}?begin={dateBeginString}&intervalSec={interval}&approxPointsCount={interval}";
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static HttpClient MakeHttpClient()
|
||||||
|
{
|
||||||
|
var httpClient = new HttpClient();
|
||||||
|
httpClient.DefaultRequestHeaders.Authorization = new("Bearer", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiZGV2IiwiaWRDb21wYW55IjoiMSIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6InJvb3QiLCJuYmYiOjE2NjY1ODY2MjAsImV4cCI6MTY5ODE0NDIyMCwiaXNzIjoiYSIsImF1ZCI6ImEifQ.zqBdR4nYB87-Xyzv025waasN47i43c9FJ23RfzIvUsM");
|
||||||
|
return httpClient;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -69,7 +69,7 @@ public class ClusterServiceTest
|
|||||||
|
|
||||||
public ClusterServiceTest()
|
public ClusterServiceTest()
|
||||||
{
|
{
|
||||||
context = TestHelpter.MakeTestContext();
|
context = TestHelpter.MakeRealTestContext();
|
||||||
wellService = new Mock<IWellService>();
|
wellService = new Mock<IWellService>();
|
||||||
context.Deposits.RemoveRange(context.Deposits);
|
context.Deposits.RemoveRange(context.Deposits);
|
||||||
context.Clusters.RemoveRange(context.Clusters);
|
context.Clusters.RemoveRange(context.Clusters);
|
||||||
|
@ -21,7 +21,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
|
|||||||
|
|
||||||
protected override ICrudService<DepositDto> MakeService()
|
protected override ICrudService<DepositDto> MakeService()
|
||||||
{
|
{
|
||||||
var dbContext = TestHelpter.MakeTestContext();
|
var dbContext = TestHelpter.MakeRealTestContext();
|
||||||
return new CrudCacheServiceBase<DepositDto, Deposit>(dbContext);
|
return new CrudCacheServiceBase<DepositDto, Deposit>(dbContext);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,7 +107,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
|
|||||||
|
|
||||||
public DetectedOperationServiceTest()
|
public DetectedOperationServiceTest()
|
||||||
{
|
{
|
||||||
context = TestHelpter.MakeTestContext();
|
context = TestHelpter.MakeRealTestContext();
|
||||||
context.SaveChanges();
|
context.SaveChanges();
|
||||||
|
|
||||||
context.Telemetries.Add(telemetry);
|
context.Telemetries.Add(telemetry);
|
||||||
|
@ -90,7 +90,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
|
|||||||
{
|
{
|
||||||
AsbCloudInfrastructure.DependencyInjection.MapsterSetup();
|
AsbCloudInfrastructure.DependencyInjection.MapsterSetup();
|
||||||
|
|
||||||
db = TestHelpter.MakeTestContext();
|
db = TestHelpter.MakeRealTestContext();
|
||||||
db.Wells.AddRange(wells);
|
db.Wells.AddRange(wells);
|
||||||
db.Companies.AddRange(companies);
|
db.Companies.AddRange(companies);
|
||||||
db.SaveChanges();
|
db.SaveChanges();
|
||||||
|
@ -19,7 +19,7 @@ public class EventServiceTest
|
|||||||
|
|
||||||
public EventServiceTest()
|
public EventServiceTest()
|
||||||
{
|
{
|
||||||
context = TestHelpter.MakeTestContext();
|
context = TestHelpter.MakeRealTestContext();
|
||||||
cacheDb = new CacheDb();
|
cacheDb = new CacheDb();
|
||||||
var telemetryTracker = new Mock<ITelemetryTracker>();
|
var telemetryTracker = new Mock<ITelemetryTracker>();
|
||||||
var imezoneServiceMock = new Mock<ITimezoneService>();
|
var imezoneServiceMock = new Mock<ITimezoneService>();
|
||||||
|
@ -17,7 +17,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
|
|||||||
public FileCategoryServiceTest()
|
public FileCategoryServiceTest()
|
||||||
{
|
{
|
||||||
|
|
||||||
context = TestHelpter.MakeTestContext();
|
context = TestHelpter.MakeRealTestContext();
|
||||||
context.SaveChanges();
|
context.SaveChanges();
|
||||||
service = new FileCategoryService(context);
|
service = new FileCategoryService(context);
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
|
|||||||
|
|
||||||
public ScheduleServiceTest()
|
public ScheduleServiceTest()
|
||||||
{
|
{
|
||||||
context = TestHelpter.MakeTestContext();
|
context = TestHelpter.MakeRealTestContext();
|
||||||
context.SaveChanges();
|
context.SaveChanges();
|
||||||
|
|
||||||
context.Deposits.Add(deposit);
|
context.Deposits.Add(deposit);
|
||||||
|
@ -40,7 +40,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
|
|||||||
timezoneService.Setup(s => s.GetByCoordinates(It.IsAny<double>(), It.IsAny<double>()))
|
timezoneService.Setup(s => s.GetByCoordinates(It.IsAny<double>(), It.IsAny<double>()))
|
||||||
.Returns(timezone);
|
.Returns(timezone);
|
||||||
|
|
||||||
context = TestHelpter.MakeTestContext();
|
context = TestHelpter.MakeRealTestContext();
|
||||||
cacheDb = new CacheDb();
|
cacheDb = new CacheDb();
|
||||||
telemetryService = new TelemetryService(context, telemetryTracker.Object, timezoneService.Object);
|
telemetryService = new TelemetryService(context, telemetryTracker.Object, timezoneService.Object);
|
||||||
|
|
||||||
@ -64,7 +64,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
|
|||||||
public async Task UpdateDataAsync()
|
public async Task UpdateDataAsync()
|
||||||
{
|
{
|
||||||
// Arrange
|
// 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 now = DateTimeOffset.UtcNow.ToOffset(TimeSpan.FromHours(timezone.Hours)).DateTime;
|
||||||
var tuser = "Завулон";
|
var tuser = "Завулон";
|
||||||
|
@ -43,7 +43,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
|
|||||||
|
|
||||||
public WellFinalDocumentsServiceTest()
|
public WellFinalDocumentsServiceTest()
|
||||||
{
|
{
|
||||||
context = TestHelpter.MakeTestContext();
|
context = TestHelpter.MakeRealTestContext();
|
||||||
context.SaveChanges();
|
context.SaveChanges();
|
||||||
|
|
||||||
fileServiceMock = new Mock<FileService>();
|
fileServiceMock = new Mock<FileService>();
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
using AsbCloudApp.Services;
|
using AsbCloudDb.Model;
|
||||||
using AsbCloudDb.Model;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
using Moq;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace AsbCloudWebApi.Tests
|
namespace AsbCloudWebApi.Tests
|
||||||
{
|
{
|
||||||
@ -9,7 +10,7 @@ namespace AsbCloudWebApi.Tests
|
|||||||
{
|
{
|
||||||
// Попробовать когда-нибудь https://github.com/MichalJankowskii/Moq.EntityFrameworkCore
|
// Попробовать когда-нибудь https://github.com/MichalJankowskii/Moq.EntityFrameworkCore
|
||||||
|
|
||||||
public static AsbCloudDbContext MakeTestContext()
|
public static AsbCloudDbContext MakeRealTestContext()
|
||||||
{
|
{
|
||||||
var options = new DbContextOptionsBuilder<AsbCloudDbContext>()
|
var options = new DbContextOptionsBuilder<AsbCloudDbContext>()
|
||||||
//.UseInMemoryDatabase(System.Guid.NewGuid().ToString())
|
//.UseInMemoryDatabase(System.Guid.NewGuid().ToString())
|
||||||
@ -22,5 +23,38 @@ namespace AsbCloudWebApi.Tests
|
|||||||
context.Database.EnsureCreated();
|
context.Database.EnsureCreated();
|
||||||
return context;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ namespace AsbCloudWebApi.Controllers
|
|||||||
/// <param name="idWell">id скважины</param>
|
/// <param name="idWell">id скважины</param>
|
||||||
/// <param name="idCategory">id категории файла</param>
|
/// <param name="idCategory">id категории файла</param>
|
||||||
/// <param name="files">Коллекция файлов</param>
|
/// <param name="files">Коллекция файлов</param>
|
||||||
/// <param name="userService">dependency</param>
|
/// <param name="userRepository">dependency</param>
|
||||||
/// <param name="token"> Токен отмены задачи </param>
|
/// <param name="token"> Токен отмены задачи </param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
@ -100,7 +100,6 @@ namespace AsbCloudWebApi.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Возвращает файл с диска на сервере
|
/// Возвращает файл с диска на сервере
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="idWell">id скважины</param>
|
|
||||||
/// <param name="idFile">id запрашиваемого файла</param>
|
/// <param name="idFile">id запрашиваемого файла</param>
|
||||||
/// <param name="token"> Токен отмены задачи </param>
|
/// <param name="token"> Токен отмены задачи </param>
|
||||||
/// <returns>Запрашиваемый файл</returns>
|
/// <returns>Запрашиваемый файл</returns>
|
||||||
@ -108,7 +107,7 @@ namespace AsbCloudWebApi.Controllers
|
|||||||
[Route("{idFile}")]
|
[Route("{idFile}")]
|
||||||
[Permission]
|
[Permission]
|
||||||
[ProducesResponseType(typeof(PhysicalFileResult), (int)System.Net.HttpStatusCode.OK)]
|
[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 idFile, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
int? idCompany = User.GetCompanyId();
|
int? idCompany = User.GetCompanyId();
|
||||||
@ -135,7 +134,7 @@ namespace AsbCloudWebApi.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="idWell">id скважины</param>
|
/// <param name="idWell">id скважины</param>
|
||||||
/// <param name="idFile">id запрашиваемого файла</param>
|
/// <param name="idFile">id запрашиваемого файла</param>
|
||||||
/// <param name="userService">dependency</param>
|
/// <param name="userRepository">dependency</param>
|
||||||
/// <param name="token">Токен отмены задачи </param>
|
/// <param name="token">Токен отмены задачи </param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpDelete("{idFile}")]
|
[HttpDelete("{idFile}")]
|
||||||
|
@ -86,7 +86,7 @@ namespace AsbCloudWebApi.Controllers.SAUB
|
|||||||
if (!isCompanyOwnsWell)
|
if (!isCompanyOwnsWell)
|
||||||
return Forbid();
|
return Forbid();
|
||||||
|
|
||||||
var content = await telemetryDataService.GetAsync(idWell, begin,
|
var content = await telemetryDataService.GetOrDefaultAsync(idWell, begin,
|
||||||
intervalSec, approxPointsCount, token).ConfigureAwait(false);
|
intervalSec, approxPointsCount, token).ConfigureAwait(false);
|
||||||
|
|
||||||
return Ok(content);
|
return Ok(content);
|
||||||
|
@ -89,7 +89,7 @@ namespace AsbCloudWebApi.Controllers.SAUB
|
|||||||
if (!isCompanyOwnsWell)
|
if (!isCompanyOwnsWell)
|
||||||
return Forbid();
|
return Forbid();
|
||||||
|
|
||||||
int? idTelemetry = telemetryService.GetIdTelemetryByIdWell(idWell);
|
int? idTelemetry = telemetryService.GetOrDefaultIdTelemetryByIdWell(idWell);
|
||||||
if (idTelemetry is null)
|
if (idTelemetry is null)
|
||||||
return NoContent();
|
return NoContent();
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ namespace AsbCloudWebApi.Controllers.WITS
|
|||||||
[FromServices] IWitsRecordRepository<TDto> witsRecordRepository,
|
[FromServices] IWitsRecordRepository<TDto> witsRecordRepository,
|
||||||
CancellationToken token)
|
CancellationToken token)
|
||||||
{
|
{
|
||||||
var idTelemetry = telemetryService.GetIdTelemetryByIdWell(idWell);
|
var idTelemetry = telemetryService.GetOrDefaultIdTelemetryByIdWell(idWell);
|
||||||
if (idTelemetry is null)
|
if (idTelemetry is null)
|
||||||
return NoContent();
|
return NoContent();
|
||||||
var dtos = await witsRecordRepository.GetLastAsync((int)idTelemetry, token);
|
var dtos = await witsRecordRepository.GetLastAsync((int)idTelemetry, token);
|
||||||
@ -96,7 +96,7 @@ namespace AsbCloudWebApi.Controllers.WITS
|
|||||||
[FromServices] IWitsRecordRepository<TDto> witsRecordRepository,
|
[FromServices] IWitsRecordRepository<TDto> witsRecordRepository,
|
||||||
CancellationToken token)
|
CancellationToken token)
|
||||||
{
|
{
|
||||||
var idTelemetry = telemetryService.GetIdTelemetryByIdWell(idWell);
|
var idTelemetry = telemetryService.GetOrDefaultIdTelemetryByIdWell(idWell);
|
||||||
if (idTelemetry is null)
|
if (idTelemetry is null)
|
||||||
return NoContent();
|
return NoContent();
|
||||||
var dtos = await witsRecordRepository.GetAsync((int)idTelemetry, begin, end, token);
|
var dtos = await witsRecordRepository.GetAsync((int)idTelemetry, begin, end, token);
|
||||||
@ -118,7 +118,7 @@ namespace AsbCloudWebApi.Controllers.WITS
|
|||||||
[FromServices] IWitsRecordRepository<TDto> witsRecordRepository,
|
[FromServices] IWitsRecordRepository<TDto> witsRecordRepository,
|
||||||
CancellationToken token)
|
CancellationToken token)
|
||||||
{
|
{
|
||||||
var idTelemetry = telemetryService.GetIdTelemetryByIdWell(idWell);
|
var idTelemetry = telemetryService.GetOrDefaultIdTelemetryByIdWell(idWell);
|
||||||
if (idTelemetry is null)
|
if (idTelemetry is null)
|
||||||
return NoContent();
|
return NoContent();
|
||||||
var dtos = await witsRecordRepository.GetStatAsync((int)idTelemetry, token);
|
var dtos = await witsRecordRepository.GetStatAsync((int)idTelemetry, token);
|
||||||
|
@ -14,25 +14,26 @@ namespace AsbCloudWebApi.Middlewares
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
class UserConnectionsLimitMiddlware
|
class UserConnectionsLimitMiddlware
|
||||||
{
|
{
|
||||||
private readonly RequestDelegate next;
|
|
||||||
private readonly int parallelRequestsToController;
|
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 ConcurrentDictionary<int, ConcurrentDictionary<string, int>> stat = new ();
|
||||||
private readonly IEnumerable<string>? controllerNames;
|
private readonly IEnumerable<string>? controllerNames;
|
||||||
|
|
||||||
public UserConnectionsLimitMiddlware(RequestDelegate next, IConfiguration configuration)
|
public UserConnectionsLimitMiddlware(RequestDelegate next, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
|
const int parallelRequestsToControllerDefault = 8;
|
||||||
this.next = next;
|
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
|
this.parallelRequestsToController = parallelRequestsToController > 0
|
||||||
? parallelRequestsToController
|
? parallelRequestsToController
|
||||||
: 5;
|
: parallelRequestsToControllerDefault;
|
||||||
|
|
||||||
controllerNames = configuration.GetSection("userLimits")?.GetValue<IEnumerable<string>>("controllerNames");
|
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>";
|
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)
|
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 userStat = stat.GetOrAdd(idUser, idUser => new());
|
||||||
var count = userStat.AddOrUpdate(controllerName, 1, (key, value) => value + 1);
|
var count = userStat.AddOrUpdate(controllerName, 0, (k, v) => v);
|
||||||
if(count < parallelRequestsToController)
|
|
||||||
|
if(count + 1 < parallelRequestsToController)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
userStat[controllerName]++;
|
||||||
await next(context);
|
await next(context);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@ -60,10 +63,9 @@ namespace AsbCloudWebApi.Middlewares
|
|||||||
{
|
{
|
||||||
context.Response.Clear();
|
context.Response.Clear();
|
||||||
context.Response.StatusCode = (int)System.Net.HttpStatusCode.TooManyRequests;
|
context.Response.StatusCode = (int)System.Net.HttpStatusCode.TooManyRequests;
|
||||||
|
|
||||||
context.Response.Headers.RetryAfter = "1000";
|
context.Response.Headers.RetryAfter = "1000";
|
||||||
context.Response.Headers.ContentType = "text/html";
|
context.Response.Headers.ContentType = "text/html";
|
||||||
await context.Response.BodyWriter.WriteAsync(body);
|
await context.Response.BodyWriter.WriteAsync(responseBody);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -2,7 +2,9 @@
|
|||||||
using AsbCloudApp.Data.DailyReport;
|
using AsbCloudApp.Data.DailyReport;
|
||||||
using AsbCloudDb;
|
using AsbCloudDb;
|
||||||
using AsbCloudDb.Model;
|
using AsbCloudDb.Model;
|
||||||
|
using AsbCloudInfrastructure;
|
||||||
using AsbCloudInfrastructure.Services.DailyReport;
|
using AsbCloudInfrastructure.Services.DailyReport;
|
||||||
|
using AsbCloudInfrastructure.Services.SAUB;
|
||||||
using ClosedXML.Excel;
|
using ClosedXML.Excel;
|
||||||
using DocumentFormat.OpenXml.Wordprocessing;
|
using DocumentFormat.OpenXml.Wordprocessing;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@ -17,7 +19,6 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace ConsoleApp1
|
namespace ConsoleApp1
|
||||||
{
|
{
|
||||||
|
|
||||||
class Program
|
class Program
|
||||||
{
|
{
|
||||||
private static AsbCloudDbContext db = ServiceFactory.Context;
|
private static AsbCloudDbContext db = ServiceFactory.Context;
|
||||||
@ -25,14 +26,13 @@ namespace ConsoleApp1
|
|||||||
// use ServiceFactory to make services
|
// use ServiceFactory to make services
|
||||||
static void Main(/*string[] args*/)
|
static void Main(/*string[] args*/)
|
||||||
{
|
{
|
||||||
var h = new Hashtable();
|
DependencyInjection.MapsterSetup();
|
||||||
h.Add("name", 1);
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||||
h.Add("name2", "66");
|
|
||||||
var v = h["v"];
|
|
||||||
|
|
||||||
var s = System.Text.Json.JsonSerializer.Serialize(h);
|
|
||||||
|
|
||||||
Console.WriteLine($"total time: ms");
|
|
||||||
|
sw.Stop();
|
||||||
|
Console.WriteLine($"total time: {sw.ElapsedMilliseconds} ms");
|
||||||
Console.ReadLine();
|
Console.ReadLine();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user