CrudService очищен от неиспользуемого кода.

CrudCacheService Адаптировано для новой схемы кеширования.
Убраны extention методы для mapster.
This commit is contained in:
ngfrolov 2022-06-06 15:43:47 +05:00
parent 570cec4162
commit 4db67113b4
30 changed files with 550 additions and 480 deletions

View File

@ -8,6 +8,6 @@
/// <summary>
/// Well id in db
/// </summary>
public int IdWell { get; set; }
int IdWell { get; set; }
}
}

View File

@ -6,6 +6,7 @@ using System.Threading.Tasks;
namespace AsbCloudApp.Services
{
#nullable enable
/// <summary>
/// Сервис получения, добавления, изменения, удаления данных
/// </summary>
@ -14,9 +15,31 @@ namespace AsbCloudApp.Services
where Tdto : Data.IId
{
/// <summary>
/// Включение связных данных
/// Код возврата ошибки: Id не найден в БД.
/// </summary>
ISet<string> Includes { get; }
public const int ErrorIdNotFound = -1;
/// <summary>
/// Получение всех записей
/// </summary>
/// <param name="token"></param>
/// <returns>emptyList if nothing found</returns>
Task<IEnumerable<Tdto>> GetAllAsync(CancellationToken token);
/// <summary>
/// Получить запись по id
/// </summary>
/// <param name="id"></param>
/// <param name="token"></param>
/// <returns>null if not found</returns>
Task<Tdto?> GetAsync(int id, CancellationToken token);
/// <summary>
/// Получить запись по id
/// </summary>
/// <param name="id"></param>
/// <returns>null if not found</returns>
Tdto? Get(int id);
/// <summary>
/// Добавление новой записи
@ -24,7 +47,7 @@ namespace AsbCloudApp.Services
/// <param name="newItem"></param>
/// <param name="token"></param>
/// <returns>Id новой записи</returns>
Task<int> InsertAsync(Tdto newItem, CancellationToken token = default);
Task<int> InsertAsync(Tdto newItem, CancellationToken token);
/// <summary>
/// Добавление нескольких записей
@ -32,23 +55,7 @@ namespace AsbCloudApp.Services
/// <param name="newItems"></param>
/// <param name="token"></param>
/// <returns>количество добавленных</returns>
Task<int> InsertRangeAsync(IEnumerable<Tdto> newItems, CancellationToken token = default);
/// <summary>
/// Получение всех записей
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
[Obsolete("Небезопасный метод, может выполняться бесконечно долго")]
Task<IEnumerable<Tdto>> GetAllAsync(CancellationToken token = default);
/// <summary>
/// Получить запись по id
/// </summary>
/// <param name="id"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<Tdto?> GetAsync(int id, CancellationToken token = default);
Task<int> InsertRangeAsync(IEnumerable<Tdto> newItems, CancellationToken token);
/// <summary>
/// Отредактировать запись
@ -56,16 +63,16 @@ namespace AsbCloudApp.Services
/// <param name="id"></param>
/// <param name="item"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> UpdateAsync(int id, Tdto item, CancellationToken token = default);
/// <returns>если больше 0 - Id записи, если меньше 0 - код ошибки</returns>
Task<int> UpdateAsync(int id, Tdto item, CancellationToken token);
/// <summary>
/// Удалить запись
/// </summary>
/// <param name="id"></param>
/// <param name="token"></param>
/// <returns></returns>
Task<int> DeleteAsync(int id, CancellationToken token = default);
/// <returns>если больше 0 - Id записи, если меньше 0 - код ошибки</returns>
Task<int> DeleteAsync(int id, CancellationToken token);
}
#nullable disable
}

View File

@ -21,6 +21,6 @@ namespace AsbCloudApp.Services
Task<IEnumerable<int>> GetClusterWellsIdsAsync(int idWell, CancellationToken token);
SimpleTimezoneDto GetTimezone(int idWell);
DatesRangeDto GetDatesRange(int idWell);
void EnshureTimezonesIsSet();
Task EnshureTimezonesIsSetAsync(CancellationToken token);
}
}

View File

@ -47,6 +47,10 @@ namespace AsbCloudInfrastructure
TypeAdapterConfig.GlobalSettings.Default.Config
.ForType<TimeOnly, TimeDto>()
.MapWith((source) => new(source));
TypeAdapterConfig.GlobalSettings.Default.Config
.ForType<TimeOnly, TimeDto>()
.MapWith((source) => new(source));
}
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
@ -98,12 +102,27 @@ namespace AsbCloudInfrastructure
services.AddTransient<IScheduleService, ScheduleService>();
// admin crud services:
services.AddTransient<ICrudService<TelemetryDto>, CrudServiceBase<TelemetryDto, Telemetry>>(); // может быть включен в сервис TelemetryService
services.AddTransient<ICrudService<TelemetryDto>, CrudServiceBase<TelemetryDto, Telemetry>>(s =>
new CrudCacheServiceBase<TelemetryDto, Telemetry>(
s.GetService<IAsbCloudDbContext>(),
dbSet => dbSet.Include(t => t.Well))); // может быть включен в сервис TelemetryService
services.AddTransient<ICrudService<DrillParamsDto>, DrillParamsService>();
services.AddTransient<ICrudService<DepositDto>, CrudCacheServiceBase<DepositDto, Deposit>>();
services.AddTransient<ICrudService<CompanyDto>, CrudCacheServiceBase<CompanyDto, Company>>();
services.AddTransient<ICrudService<DepositDto>, CrudCacheServiceBase<DepositDto, Deposit>>(s =>
new CrudCacheServiceBase<DepositDto, Deposit>(
s.GetService<IAsbCloudDbContext>(),
dbSet => dbSet.Include(d => d.Clusters)));
services.AddTransient<ICrudService<CompanyDto>, CrudCacheServiceBase<CompanyDto, Company>>(s =>
new CrudCacheServiceBase<CompanyDto, Company>(
s.GetService<IAsbCloudDbContext>(),
dbSet => dbSet.Include(c=>c.CompanyType)));
services.AddTransient<ICrudService<CompanyTypeDto>, CrudCacheServiceBase<CompanyTypeDto, CompanyType>>();
services.AddTransient<ICrudService<ClusterDto>, CrudCacheServiceBase<ClusterDto, Cluster>>(); // может быть включен в сервис ClusterService
services.AddTransient<ICrudService<ClusterDto>, CrudCacheServiceBase<ClusterDto, Cluster>>(s =>
new CrudCacheServiceBase<ClusterDto, Cluster>(
s.GetService<IAsbCloudDbContext>(),
dbSet => dbSet
.Include(c => c.Wells)
.Include(c => c.Deposit))); // может быть включен в сервис ClusterService
services.AddTransient<ICrudService<PermissionDto>, CrudCacheServiceBase<PermissionDto, Permission>>();
// TelemetryData services

View File

@ -26,6 +26,57 @@ namespace AsbCloudInfrastructure.EfCache
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)
@ -233,15 +284,9 @@ namespace AsbCloudInfrastructure.EfCache
where TKey : notnull
{
IDictionary factory()
{
var queryData = query.AsNoTracking()
.ToDictionary(keySelector);
return queryData;
}
=> query.AsNoTracking().ToDictionary(keySelector);
var cache = GetOrAddCache(tag, factory, obsolescence);
if (cache.Data is Dictionary<TKey, TEntity> typedData)
return typedData;
throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique.");
return cache.GetData<TKey, TEntity>();
}
/// <summary>
@ -267,17 +312,9 @@ namespace AsbCloudInfrastructure.EfCache
where TKey : notnull
{
IDictionary factory()
{
var queryData = query.AsNoTracking()
.ToList();
var data = queryData
.ToDictionary(keySelector, convert);
return data;
}
=> query.AsNoTracking().ToDictionary(keySelector);
var cache = GetOrAddCache(tag, factory, obsolescence);
if (cache.Data is Dictionary<TKey, TModel> typedData)
return typedData;
throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique.");
return cache.GetData<TKey, TEntity, TModel>(convert);
}
/// <summary>
@ -301,15 +338,9 @@ namespace AsbCloudInfrastructure.EfCache
where TKey : notnull
{
async Task<IDictionary> factory(CancellationToken token)
{
var queryData = await query.AsNoTracking().ToDictionaryAsync(keySelector, token);
return queryData;
}
=> await query.AsNoTracking().ToDictionaryAsync(keySelector, token);
var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token);
if (cache.Data is Dictionary<TKey, TEntity> typedData)
return typedData;
throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique.");
return cache.GetData<TKey, TEntity>();
}
/// <summary>
@ -331,16 +362,9 @@ namespace AsbCloudInfrastructure.EfCache
where TKey : notnull
{
async Task<IDictionary> factory(CancellationToken token)
{
var queryData = await query.AsNoTracking().ToListAsync(token);
var data = queryData.ToDictionary(keySelector, convert);
return data;
}
=> await query.AsNoTracking().ToDictionaryAsync(keySelector, token);
var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token);
if (cache.Data is Dictionary<TKey, TModel> typedData)
return typedData;
throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique.");
return cache.GetData<TKey, TEntity, TModel>(convert);
}
/// <summary>

View File

@ -26,6 +26,54 @@ namespace AsbCloudInfrastructure.EfCache
internal DateTime DateObsolete;
internal DateTime DateObsoleteTotal;
internal readonly SemaphoreSlim semaphore = new(1);
internal IEnumerable<TEntity> GetData<TEntity>()
{
if (Data is IEnumerable<TEntity> typedData)
return typedData;
throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique.");
}
internal IEnumerable<TModel> GetData<TEntity, TModel>(Func<TEntity, TModel> convert, int attempt = 1)
{
if (Data is IEnumerable<TModel> typedData)
return typedData;
if (Data is IEnumerable<TEntity> typedEntityData)
{
if (semaphore.Wait(0))
{
try
{
var convertedData = typedEntityData.Select(convert).ToList();
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(convert, --attempt);
throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique.");
}
}
private static CacheItem GetOrAddCache(string tag, Func<IEnumerable> valueFactory, TimeSpan obsolete)
@ -224,16 +272,9 @@ namespace AsbCloudInfrastructure.EfCache
public static IEnumerable<TEntity> FromCache<TEntity>(this IQueryable<TEntity> query, string tag, TimeSpan obsolescence)
where TEntity : class
{
IEnumerable factory()
{
var queryData = query.AsNoTracking()
.ToList();
return queryData;
}
IEnumerable factory() => query.AsNoTracking().ToList();
var cache = GetOrAddCache(tag, factory, obsolescence);
if(cache.Data is IEnumerable<TEntity> typedData)
return typedData;
throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique.");
return cache.GetData<TEntity>();
}
/// <summary>
@ -250,19 +291,9 @@ namespace AsbCloudInfrastructure.EfCache
public static IEnumerable<TModel> FromCache<TEntity, TModel>(this IQueryable<TEntity> query, string tag, TimeSpan obsolescence, Func<TEntity, TModel> convert)
where TEntity : class
{
IEnumerable factory ()
{
var queryData = query.AsNoTracking()
.ToList();
var data = queryData
.Select(convert)
.ToList();
return data;
}
IEnumerable factory () => query.AsNoTracking().ToList();
var cache = GetOrAddCache(tag, factory, obsolescence);
if (cache.Data is IEnumerable<TModel> typedData)
return typedData;
throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique.");
return cache.GetData(convert);
}
/// <summary>
@ -278,14 +309,9 @@ namespace AsbCloudInfrastructure.EfCache
where TEntity : class
{
async Task<IEnumerable> factory(CancellationToken token)
{
var queryData = await query.AsNoTracking().ToListAsync(token);
return queryData;
}
=> await query.AsNoTracking().ToListAsync(token);
var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token);
if (cache.Data is IEnumerable<TEntity> typedData)
return typedData;
throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique.");
return cache.GetData<TEntity>();
}
/// <summary>
@ -304,17 +330,9 @@ namespace AsbCloudInfrastructure.EfCache
where TEntity : class
{
async Task<IEnumerable> factory(CancellationToken token)
{
var queryData = await query.AsNoTracking().ToListAsync(token);
var data = queryData
.Select(convert)
.ToList();
return data;
}
=> await query.AsNoTracking().ToListAsync(token);
var cache = await GetOrAddCacheAsync(tag, factory, obsolescence, token);
if (cache.Data is IEnumerable<TModel> typedData)
return typedData;
throw new TypeAccessException("Cache data has wrong type. Possible 'tag' is not unique.");
return cache.GetData(convert);
}
/// <summary>

View File

@ -6,35 +6,11 @@ namespace Mapster
{
public static class MapsterExtension
{
public static IEnumerable<TDestination> Adapt<TDestination>(this IEnumerable<object> sourceList)
{
return sourceList.Select(item => item.Adapt<TDestination>());
}
//public static IEnumerable<TDestination> Adapt<TDestination>(this IEnumerable<object> sourceList)
//{
// return sourceList.Select(item => item.Adapt<TDestination>());
//}
public static TDestination Adapt<TDestination>(this object source, Action<TDestination> afterMapAction = default)
{
var dest = source.Adapt<TDestination>();
afterMapAction?.Invoke(dest);
return dest;
}
public static TDestination Adapt<TDestination, TSource>(this TSource source, Action<TDestination, TSource> afterMapAction = default)
{
var dest = source.Adapt<TDestination>();
afterMapAction?.Invoke(dest, source);
return dest;
}
public static IEnumerable<TDestination> Adapt<TDestination, TSource>(this IEnumerable<TSource> sourceList, Action<TDestination, TSource> eachAfterMapAction = default)
{
foreach (var item in sourceList)
{
var dest = item.Adapt<TDestination>();
eachAfterMapAction?.Invoke(dest, item);
yield return dest;
}
}
}
}

View File

@ -71,7 +71,7 @@ namespace AsbCloudInfrastructure.Services
.ToListAsync(token)
.ConfigureAwait(false);
var dtos = entities.Adapt<ClusterDto>();
var dtos = entities.Adapt<IEnumerable<ClusterDto>>();
return dtos;
}
@ -87,7 +87,7 @@ namespace AsbCloudInfrastructure.Services
.ToListAsync(token)
.ConfigureAwait(false);
var dtos = entities.Adapt<ClusterDto>();
var dtos = entities.Adapt<IEnumerable<ClusterDto>>();
return dtos;
}

View File

@ -1,7 +1,6 @@
using AsbCloudApp.Services;
using AsbCloudDb.Model;
using AsbCloudInfrastructure.Services.Cache;
using Mapster;
using AsbCloudInfrastructure.EfCache;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
@ -10,111 +9,104 @@ using System.Threading.Tasks;
namespace AsbCloudInfrastructure.Services
{
public class CrudCacheServiceBase<TDto, TModel> : ICrudService<TDto>
#nullable enable
/// <summary>
/// CRUD ñåðâèñ ñ êåøåì â îïåðàòèâêå
/// </summary>
/// <typeparam name="TDto"></typeparam>
/// <typeparam name="TEntity"></typeparam>
public class CrudCacheServiceBase<TDto, TEntity>: CrudServiceBase<TDto, TEntity>
where TDto : AsbCloudApp.Data.IId
where TModel : class, AsbCloudDb.Model.IId
where TEntity : class, AsbCloudDb.Model.IId
{
private CacheTable<TModel> cache = null;
private readonly IAsbCloudDbContext db;
private readonly CacheDb cacheDb;
protected string CacheTag = typeof(TDto).Name;
protected TimeSpan CacheOlescence = TimeSpan.FromMinutes(5);
protected int KeySelector(TEntity entity) => entity.Id;
public ISet<string> Includes { get; } = new SortedSet<string>();
public CrudCacheServiceBase(IAsbCloudDbContext dbContext)
: base(dbContext) { }
protected CacheTable<TModel> Cache
public CrudCacheServiceBase(IAsbCloudDbContext dbContext, ISet<string> includes)
: base(dbContext, includes) { }
public CrudCacheServiceBase(IAsbCloudDbContext dbContext, Func<DbSet<TEntity>, IQueryable<TEntity>> makeQuery)
: base(dbContext, makeQuery) { }
/// <inheritdoc/>
public override async Task<int> InsertAsync(TDto newItem, CancellationToken token)
{
get
{
if (cache is null)
cache = cacheDb.GetCachedTable<TModel>((AsbCloudDbContext)db, Includes);
return cache;
}
var result = await base.InsertAsync(newItem, token);
if (result > 0)
DropCache();
return result;
}
public CrudCacheServiceBase(IAsbCloudDbContext db, CacheDb cacheDb)
/// <inheritdoc/>
public override async Task<int> InsertRangeAsync(IEnumerable<TDto> dtos, CancellationToken token)
{
this.db = db;
this.cacheDb = cacheDb;
var result = await base.InsertRangeAsync(dtos, token);
if (result > 0)
DropCache();
return result;
}
public virtual async Task<int> InsertAsync(TDto newItem, CancellationToken token = default)
/// <inheritdoc/>
public override async Task<IEnumerable<TDto>> GetAllAsync(CancellationToken token)
{
var entity = Convert(newItem);
var insertedEntity = await Cache.InsertAsync(entity, token)
.ConfigureAwait(false);
return insertedEntity?.Id ?? -1;
var result = await GetQuery()
.FromCacheDictionaryAsync(CacheTag, CacheOlescence, KeySelector, Convert, token);
return result.Values;
}
public virtual async Task<int> InsertRangeAsync(IEnumerable<TDto> dtos, CancellationToken token)
/// <summary>
/// Ñèíõðîííî ïîëó÷èòü çàïèñü ïî ÈÄ
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public override TDto? Get(int id)
{
var entities = dtos.Select(Convert);
var insertedEntities = await Cache.InsertAsync(entities, token)
.ConfigureAwait(false);
return insertedEntities?.Count() ?? 0;
var result = GetQuery()
.FromCacheDictionary(CacheTag, CacheOlescence, KeySelector, Convert);
return result.GetValueOrDefault(id);
}
public virtual async Task<IEnumerable<TDto>> GetAllAsync(CancellationToken token)
/// <inheritdoc/>
public override async Task<TDto?> GetAsync(int id, CancellationToken token)
{
var entities = await Cache.WhereAsync(token)
.ConfigureAwait(false);
var dtos = entities?.Select(Convert);
return dtos;
var result = await GetQuery()
.FromCacheDictionaryAsync(CacheTag, CacheOlescence, KeySelector, Convert, token);
return result.GetValueOrDefault(id);
}
public virtual async Task<TDto> GetAsync(int id, CancellationToken token)
/// <inheritdoc/>
public override async Task<int> UpdateAsync(int id, TDto dto, CancellationToken token)
{
var entity = await Cache
.FirstOrDefaultAsync(p => p.Id == id, token)
.ConfigureAwait(false);
if (entity is null)
return default;
var dto = Convert(entity);
return dto;
var result = await base.UpdateAsync(id, dto, token);
if (result > 0)
DropCache();
return result;
}
public virtual async Task<int> UpdateAsync(int id, TDto dto, CancellationToken token)
/// <inheritdoc/>
public override async Task<int> DeleteAsync(int id, CancellationToken token)
{
if (dto.Id != id)
{
var exist = await Cache.ContainsAsync(i => i.Id == dto.Id, token)
.ConfigureAwait(false);
if (exist)
return -1;
await Cache.RemoveAsync(i => i.Id == id, token)
.ConfigureAwait(false);
}
var entity = Convert(dto);
await Cache.UpsertAsync(entity, token)
.ConfigureAwait(false);
return 1;
var result = await base.DeleteAsync(id, token);
if (result > 0)
DropCache();
return result;
}
public virtual async Task<int> DeleteAsync(int id, CancellationToken token)
{
var affected = await Cache.RemoveAsync(p => p.Id == id, token)
.ConfigureAwait(false);
return affected;
}
protected virtual Task<Dictionary<int, TDto>> GetCacheAsync(CancellationToken token)
=> GetQuery()
.FromCacheDictionaryAsync(CacheTag, CacheOlescence, KeySelector, Convert, token);
public virtual async Task<int> DeleteAsync(IEnumerable<int> ids, CancellationToken token = default)
{
var affected = await Cache.RemoveAsync(p => ids.Contains(p.Id), token)
.ConfigureAwait(false);
return affected;
}
protected virtual TModel Convert(TDto src)
{
var entity = src.Adapt<TModel>();
return entity;
}
protected virtual Dictionary<int, TDto> GetCache()
=> GetQuery()
.FromCacheDictionary(CacheTag, CacheOlescence, KeySelector, Convert);
protected virtual TDto Convert(TModel src)
{
var dto = src.Adapt<TDto>();
return dto;
}
protected virtual void DropCache()
=> dbSet.DropCacheDictionary(CacheTag);
}
#nullable disable
}

View File

@ -1,5 +1,4 @@
using AsbCloudApp.Data;
using AsbCloudApp.Services;
using AsbCloudApp.Services;
using AsbCloudDb.Model;
using Mapster;
using Microsoft.EntityFrameworkCore;
@ -11,87 +10,99 @@ using System.Threading.Tasks;
namespace AsbCloudInfrastructure.Services
{
public class CrudServiceBase<TDto, TModel> : ICrudService<TDto>, IConverter<TDto, TModel>
#nullable enable
/// <summary>
/// CRUD сервис для работы с БД
/// </summary>
/// <typeparam name="TDto"></typeparam>
/// <typeparam name="TEntity"></typeparam>
public class CrudServiceBase<TDto, TEntity> : ICrudService<TDto>
where TDto : AsbCloudApp.Data.IId
where TModel : class, AsbCloudDb.Model.IId
where TEntity : class, AsbCloudDb.Model.IId
{
protected readonly IAsbCloudDbContext context;
protected readonly DbSet<TModel> dbSet;
public ISet<string> Includes { get; } = new SortedSet<string>();
protected readonly IAsbCloudDbContext dbContext;
protected readonly DbSet<TEntity> dbSet;
protected readonly Func<IQueryable<TEntity>> GetQuery;
public CrudServiceBase(IAsbCloudDbContext context)
{
this.context = context;
dbSet = context.Set<TModel>();
this.dbContext = context;
dbSet = context.Set<TEntity>();
GetQuery = () => dbSet;
}
public virtual async Task<PaginationContainer<TDto>> GetPageAsync(int skip = 0, int take = 32, CancellationToken token = default)
public CrudServiceBase(IAsbCloudDbContext dbContext, ISet<string> includes)
{
var query = GetQueryWithIncludes();
var count = await query
.CountAsync(token)
.ConfigureAwait(false);
this.dbContext = dbContext;
dbSet = dbContext.Set<TEntity>();
var container = new PaginationContainer<TDto>
{
Skip = skip,
Take = take,
Count = count,
GetQuery = () => {
IQueryable<TEntity> query = dbSet;
foreach (var include in includes)
query = query.Include(include);
return query;
};
if (skip >= count)
return container;
query = query
.OrderBy(e => e.Id);
if (skip > 0)
query = query.Skip(skip);
query = query.Take(take);
var entities = await query
.ToListAsync(token)
.ConfigureAwait(false);
container.Items = entities
.Select(entity => Convert(entity))
.ToList();
return container;
}
public CrudServiceBase(IAsbCloudDbContext context, Func<DbSet<TEntity>, IQueryable<TEntity>> makeQuery)
{
this.dbContext = context;
dbSet = context.Set<TEntity>();
GetQuery = () => makeQuery(dbSet);
}
/// <inheritdoc/>
public virtual async Task<IEnumerable<TDto>> GetAllAsync(CancellationToken token = default)
{
var query = GetQueryWithIncludes();
var entities = await query
.OrderBy(e => e.Id)
.ToListAsync(token).ConfigureAwait(false);
var dto = entities.Select(Convert).ToList();
return dto;
var entities = await GetQuery()
//.OrderBy(e => e.Id)
.AsNoTracking()
.ToListAsync(token)
.ConfigureAwait(false);
var dtos = entities.Select(Convert).ToList();
return dtos;
}
public virtual async Task<TDto> GetAsync(int id, CancellationToken token = default)
/// <inheritdoc/>
public virtual async Task<TDto?> GetAsync(int id, CancellationToken token = default)
{
var query = GetQueryWithIncludes();
var entity = await query
.FirstOrDefaultAsync(e => e.Id == id, token).ConfigureAwait(false);
var entity = await GetQuery()
.AsNoTracking()
.FirstOrDefaultAsync(e => e.Id == id, token)
.ConfigureAwait(false);
if (entity == default)
return default;
var dto = Convert(entity);
return dto;
}
/// <inheritdoc/>
public virtual TDto? Get(int id)
{
var entity = GetQuery()
.AsNoTracking()
.FirstOrDefault(e => e.Id == id);
if (entity == default)
return default;
var dto = Convert(entity);
return dto;
}
/// <inheritdoc/>
public virtual async Task<int> InsertAsync(TDto item, CancellationToken token = default)
{
var entity = Convert(item);
entity.Id = 0;
dbSet.Add(entity);
await context.SaveChangesAsync(token);
await dbContext.SaveChangesAsync(token);
return entity.Id;
}
/// <inheritdoc/>
public virtual Task<int> InsertRangeAsync(IEnumerable<TDto> items, CancellationToken token = default)
{
if (!items.Any())
return Task.FromResult(0);
var entities = items.Select(i =>
{
var entity = Convert(i);
@ -100,40 +111,40 @@ namespace AsbCloudInfrastructure.Services
});
dbSet.AddRange(entities);
return context.SaveChangesAsync(token);
return dbContext.SaveChangesAsync(token);
}
/// <inheritdoc/>
public virtual async Task<int> UpdateAsync(int id, TDto item, CancellationToken token = default)
{
var existingEntity = await dbSet.AsNoTracking().FirstOrDefaultAsync(e => e.Id == id, token).ConfigureAwait(false);
var existingEntity = await dbSet
.AsNoTracking()
.FirstOrDefaultAsync(e => e.Id == id, token)
.ConfigureAwait(false);
if (existingEntity is null)
return 0;
return ICrudService<TDto>.ErrorIdNotFound;
var entity = Convert(item);
entity.Id = id;
dbSet.Update(entity);
return await context.SaveChangesAsync(token);
var entry = dbSet.Update(entity);
await dbContext.SaveChangesAsync(token);
return entry.Entity.Id;
}
/// <inheritdoc/>
public virtual Task<int> DeleteAsync(int id, CancellationToken token = default)
{
var entity = dbSet.AsNoTracking()
var entity = dbSet
.AsNoTracking()
.FirstOrDefault(e => e.Id == id);
if (entity == default)
return Task.FromResult(0);
return Task.FromResult(ICrudService<TDto>.ErrorIdNotFound);
dbSet.Remove(entity);
return context.SaveChangesAsync(token);
return dbContext.SaveChangesAsync(token);
}
public virtual TDto Convert(TModel src) => src.Adapt<TDto>();
protected virtual TDto Convert(TEntity src) => src.Adapt<TDto>();
public virtual TModel Convert(TDto src) => src.Adapt<TModel>();
protected IQueryable<TModel> GetQueryWithIncludes()
{
IQueryable<TModel> query = dbSet;
foreach (var include in Includes)
query = query.Include(include);
return query;
}
protected virtual TEntity Convert(TDto src) => src.Adapt<TEntity>();
}
#nullable disable
}

View File

@ -77,7 +77,7 @@ namespace AsbCloudInfrastructure.Services
.ToListAsync(token)
.ConfigureAwait(false);
var dto = entities.Adapt<DrillParamsDto>();
var dto = entities.Adapt<IEnumerable<DrillParamsDto>>();
return dto;
}
@ -95,7 +95,7 @@ namespace AsbCloudInfrastructure.Services
.ToListAsync(token)
.ConfigureAwait(false);
var compositeDrillParamsDtos = compositeWellDrillParams.Adapt<DrillParamsDto>();
var compositeDrillParamsDtos = compositeWellDrillParams.Adapt<IEnumerable<DrillParamsDto>>();
return compositeDrillParamsDtos;
}

View File

@ -75,7 +75,7 @@ namespace AsbCloudInfrastructure.Services.DrillingProgram
.SelectMany(r => r.Company.Users)
.Where(u => u != null && !string.IsNullOrEmpty(u.Email))
.ToListAsync(token);
var usersDto = users.Adapt<UserDto>();
var usersDto = users.Adapt<IEnumerable<UserDto>>();
return usersDto;
}

View File

@ -46,11 +46,7 @@ namespace AsbCloudInfrastructure.Services
.ConfigureAwait(false);
var timezone = wellService.GetTimezone(idWell);
var dto = entity?.Adapt<MeasureDto, Measure>((d, s) =>
{
d.CategoryName = s.Category?.Name;
d.Timestamp = s.Timestamp.ToRemoteDateTime(timezone.Hours);
});
var dto = Convert(entity, timezone.Hours);
return dto;
}
@ -69,11 +65,7 @@ namespace AsbCloudInfrastructure.Services
.ConfigureAwait(false);
var timezone = wellService.GetTimezone(idWell);
var dtos = entities.Adapt<MeasureDto, Measure>((d, s) =>
{
d.CategoryName = s.Category?.Name;
d.Timestamp = s.Timestamp.ToRemoteDateTime(timezone.Hours);
});
var dtos = entities.Select(e => Convert(e, timezone.Hours));
return dtos;
}
@ -84,9 +76,8 @@ namespace AsbCloudInfrastructure.Services
if (dto.Data is null)
throw new ArgumentInvalidException("data.data is not optional", nameof(dto));
var timezone = wellService.GetTimezone(idWell);
var entity = dto.Adapt<Measure>();
var entity = Convert(dto, timezone.Hours);
entity.IdWell = idWell;
entity.Timestamp = dto.Timestamp.ToUtcDateTimeOffset(timezone.Hours);
db.Measures.Add(entity);
return db.SaveChangesAsync(token);
}
@ -109,7 +100,6 @@ namespace AsbCloudInfrastructure.Services
throw new ArgumentInvalidException("id doesn't exist", nameof(dto));
var timezone = wellService.GetTimezone(idWell);
entity.IdWell = idWell;
entity.Timestamp = dto.Timestamp.ToUtcDateTimeOffset(timezone.Hours);
entity.Data = (RawData)dto.Data;
@ -138,5 +128,20 @@ namespace AsbCloudInfrastructure.Services
db.Measures.RemoveRange(db.Measures.Where(m => m.IdWell == idWell && m.Id == idData));
return db.SaveChangesAsync(token);
}
private MeasureDto Convert(Measure entity, double hours)
{
var dto = entity.Adapt<MeasureDto>();
dto.CategoryName = entity.Category?.Name;
dto.Timestamp = entity.Timestamp.ToRemoteDateTime(hours);
return dto;
}
private Measure Convert(MeasureDto dto, double hours)
{
var entity = dto.Adapt<Measure>();
entity.Category = null;
entity.Timestamp = dto.Timestamp.ToUtcDateTimeOffset(hours);
return entity;
}
}
}

View File

@ -51,7 +51,7 @@ namespace AsbCloudInfrastructure.Services.SAUB
return telemetryDtos;
var telemetries = cacheTelemetry
.Where(t => activeTelemetriesUids.Contains(t.RemoteUid));
telemetryDtos = telemetries.Adapt<TelemetryDto>().ToList();
telemetryDtos = telemetries.Adapt<IEnumerable<TelemetryDto>>().ToList();
return telemetryDtos;
}

View File

@ -16,15 +16,15 @@ namespace AsbCloudInfrastructure.Services
{
private readonly IWellService wellService;
public ScheduleService(IAsbCloudDbContext context, IWellService wellService) : base(context)
public ScheduleService(IAsbCloudDbContext context, IWellService wellService)
: base(context, dbSet => dbSet.Include(s => s.Driller))
{
Includes.Add(nameof(Schedule.Driller));
this.wellService = wellService;
}
public async Task<IEnumerable<ScheduleDto>> GetByIdWellAsync(int idWell, CancellationToken token = default)
{
var entities = await GetQueryWithIncludes()
var entities = await GetQuery()
.Where(s => s.IdWell == idWell)
.ToListAsync(token);
var dtos = entities.Select(Convert);
@ -36,7 +36,7 @@ namespace AsbCloudInfrastructure.Services
var hoursOffset = wellService.GetTimezone(idWell).Hours;
var date = workTime.ToUtcDateTimeOffset(hoursOffset);
var entities = await GetQueryWithIncludes()
var entities = await GetQuery()
.Where(s => s.IdWell==idWell
&& s.DrillStart <= date
&& s.DrillEnd >= date)
@ -56,7 +56,7 @@ namespace AsbCloudInfrastructure.Services
return entity?.Driller.Adapt<DrillerDto>();
}
public override Schedule Convert(ScheduleDto dto)
protected override Schedule Convert(ScheduleDto dto)
{
var hoursOffset = wellService.GetTimezone(dto.IdWell).Hours;
var entity = base.Convert(dto);
@ -65,7 +65,7 @@ namespace AsbCloudInfrastructure.Services
return entity;
}
public override ScheduleDto Convert(Schedule entity)
protected override ScheduleDto Convert(Schedule entity)
{
var hoursOffset = wellService.GetTimezone(entity.IdWell).Hours;
var dto = base.Convert(entity);

View File

@ -56,6 +56,14 @@ namespace AsbCloudInfrastructure.Services
var dtos = entities?.Select(Convert);
return dtos;
}
public UserRoleDto Get(int id)
{
var entity = cacheUserRoles.FirstOrDefault(r => r.Id == id);
if (entity is null)
return null;
var dto = Convert(entity);
return dto;
}
public async Task<UserRoleDto> GetAsync(int id, CancellationToken token = default)
{
@ -97,7 +105,7 @@ namespace AsbCloudInfrastructure.Services
.ConfigureAwait(false);
if (exist)
return -1;
return ICrudService<UserRoleDto>.ErrorIdNotFound;
await cacheUserRoles.RemoveAsync(i => i.Id == id, token)
.ConfigureAwait(false);
@ -107,10 +115,9 @@ namespace AsbCloudInfrastructure.Services
await UpdatePermissionsAsync(dto, token);
await UpdateIncludedRolesAsync(dto, token);
await cacheUserRoles.UpsertAsync(entity, token)
var result = await cacheUserRoles.UpsertAsync(entity, token)
.ConfigureAwait(false);
return dto.Id;
return result;
}
public IEnumerable<UserRoleDto> GetNestedById(int id, int recursionLevel = 7)

View File

@ -81,6 +81,14 @@ namespace AsbCloudInfrastructure.Services
return dtos;
}
public UserExtendedDto Get(int id)
{
var entity = cacheUsers.FirstOrDefault(u => u.Id == id);
var dto = Convert(entity);
dto.RoleNames = GetRolesNamesByIdUser(dto.Id);
return dto;
}
public async Task<UserExtendedDto> GetAsync(int id, CancellationToken token = default)
{
var entity = await cacheUsers.FirstOrDefaultAsync(u => u.Id == id, token).ConfigureAwait(false);

View File

@ -26,7 +26,7 @@ namespace AsbCloudInfrastructure.Services
.AsNoTracking()
.ToListAsync(token)
.ConfigureAwait(false);
return entities.Adapt<WellCompositeDto>();
return entities.Select(Convert);
}
public Task<int> SaveAsync(int idWell, IEnumerable<WellCompositeDto> wellComposites, CancellationToken token)
@ -35,10 +35,22 @@ namespace AsbCloudInfrastructure.Services
.Where(c => c.IdWell == idWell));
var entities = wellComposites
.Adapt<WellComposite, WellCompositeDto>((s, _) => { s.IdWell = idWell; });
.Select(w => Convert(idWell, w));
context.WellComposites.AddRange(entities);
return context.SaveChangesAsync(token);
}
private WellComposite Convert(int idWell, WellCompositeDto dto)
{
var entity = dto.Adapt<WellComposite>();
entity.IdWell = idWell;
return entity;
}
private WellCompositeDto Convert(WellComposite entity)
{
var dto = entity.Adapt<WellCompositeDto>();
return dto;
}
}
}

View File

@ -46,8 +46,7 @@ namespace AsbCloudInfrastructure.Services.WellOperationService
{
var operationTypes = cachedOperationCategories
.Distinct().OrderBy(o => o.Name);
var result = operationTypes.Adapt<WellOperationCategoryDto>();
var result = operationTypes.Adapt<IEnumerable<WellOperationCategoryDto>>();
return result;
}

View File

@ -3,7 +3,9 @@ using AsbCloudApp.Exceptions;
using AsbCloudApp.Services;
using AsbCloudDb.Model;
using AsbCloudInfrastructure.Services.Cache;
using AsbCloudInfrastructure.EfCache;
using Mapster;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
@ -25,31 +27,53 @@ namespace AsbCloudInfrastructure.Services
dst => dst.WellType)
.Config;
private const string relationCompaniesWellsCacheTag = "RelationCompaniesWells";
private static readonly TimeSpan relationCompaniesWellsCacheObsolence = TimeSpan.FromMinutes(15);
private readonly ITelemetryService telemetryService;
private readonly CacheTable<RelationCompanyWell> cacheRelationCompaniesWells;
private readonly CacheTable<CompanyType> cacheCompanyWellTypes;
private readonly ICrudService<CompanyTypeDto> companyTypesService;
private readonly ITimezoneService timezoneService;
private readonly Lazy<IWellOperationService> wellOperationService;
private readonly IWellOperationService wellOperationService;
public ITelemetryService TelemetryService => telemetryService;
private static IQueryable<Well> MakeQueryWell(DbSet<Well> dbSet)
=> dbSet
.Include(w => w.Cluster)
.ThenInclude(c => c.Deposit)
.Include(w => w.Telemetry)
.Include(w => w.WellType)
.Include(w => w.RelationCompaniesWells)
.ThenInclude(r => r.Company);
public WellService(IAsbCloudDbContext db, CacheDb cacheDb, ITelemetryService telemetryService, ITimezoneService timezoneService)
: base(db, cacheDb)
: base(db, MakeQueryWell)
{
this.telemetryService = telemetryService;
this.timezoneService = timezoneService;
this.wellOperationService = new Lazy<IWellOperationService>(() => new WellOperationService.WellOperationService(db, cacheDb, this));
cacheRelationCompaniesWells = cacheDb.GetCachedTable<RelationCompanyWell>((AsbCloudDbContext)db, nameof(RelationCompanyWell.Company), nameof(RelationCompanyWell.Well));
cacheCompanyWellTypes = cacheDb.GetCachedTable<CompanyType>((AsbCloudDbContext)db);
Includes.Add($"{nameof(Well.Cluster)}.{nameof(Cluster.Deposit)}");
Includes.Add(nameof(Well.Telemetry));
Includes.Add($"{nameof(Well.RelationCompaniesWells)}.{nameof(RelationCompanyWell.Company)}");
Includes.Add(nameof(Well.WellType));
this.wellOperationService = new WellOperationService.WellOperationService(db, cacheDb, this);
companyTypesService = new CrudCacheServiceBase<CompanyTypeDto, CompanyType>(dbContext);
}
private IEnumerable<RelationCompanyWell> GetCacheRelationCompanyWell()
=> dbContext.RelationCompaniesWells
.Include(r => r.Company)
.Include(r => r.Well)
.FromCache(relationCompaniesWellsCacheTag, relationCompaniesWellsCacheObsolence);
private Task<IEnumerable<RelationCompanyWell>> GetCacheRelationCompanyWellAsync(CancellationToken token)
=> dbContext.RelationCompaniesWells
.Include(r => r.Company)
.Include(r => r.Well)
.FromCacheAsync(relationCompaniesWellsCacheTag, relationCompaniesWellsCacheObsolence, token);
private void DropCacheRelationCompanyWell()
=> dbContext.RelationCompaniesWells.DropCache(relationCompaniesWellsCacheTag);
public DateTimeOffset GetLastTelemetryDate(int idWell)
{
var well = Cache.FirstOrDefault(w => w.Id == idWell);
var well = Get(idWell);
if (well?.IdTelemetry is null)
return DateTimeOffset.MinValue;
@ -60,14 +84,17 @@ namespace AsbCloudInfrastructure.Services
public async Task<IEnumerable<WellDto>> GetWellsByCompanyAsync(int idCompany, CancellationToken token)
{
var relations = await cacheRelationCompaniesWells
.WhereAsync(r => r.IdCompany == idCompany, token);
var relationsCache = await GetCacheRelationCompanyWellAsync(token);
var wellsIds = relations.Select(r => r.IdWell);
var wells = await Cache.WhereAsync(w => wellsIds.Contains(w.Id), token);
var wellsIds = relationsCache
.Where(r => r.IdCompany == idCompany)
.Select(r => r.IdWell);
var wellsDtos = (await GetCacheAsync(token))
.Where(kv => wellsIds.Contains(kv.Key))
.Select(kv =>kv.Value);
var dtos = wells.Select(Convert);
return dtos;
return wellsDtos.ToList();
}
public override async Task<int> InsertAsync(WellDto dto, CancellationToken token = default)
@ -78,20 +105,22 @@ namespace AsbCloudInfrastructure.Services
if (dto.IdState is < 0 or > 2)
throw new ArgumentInvalidException("Текущее состояние работы скважины указано неправильно.", nameof(dto));
if (dto.Id != 0 && await Cache.ContainsAsync(w => w.Id == dto.Id, token))
if (dto.Id != 0 && (await GetCacheAsync(token)).ContainsKey(dto.Id))
throw new ArgumentInvalidException($"Нельзя повторно добавить скважину с id: {dto.Id}", nameof(dto));
var entity = Convert(dto);
var result = await Cache.InsertAsync(entity, token);
var result = await base.InsertAsync(dto, token);
if (dto.Companies.Any())
{
var newRelations = dto.Companies.Select(c => new RelationCompanyWell { IdWell = result.Id, IdCompany = c.Id });
await cacheRelationCompaniesWells.InsertAsync(newRelations, token);
var newRelations = dto.Companies.Select(c => new RelationCompanyWell { IdWell = result, IdCompany = c.Id });
dbContext.RelationCompaniesWells.AddRange(newRelations);
await dbContext.SaveChangesAsync(token);
DropCacheRelationCompanyWell();
}
return result.Id;
return result;
}
public override Task<int> InsertRangeAsync(IEnumerable<WellDto> dtos, CancellationToken token)
@ -111,47 +140,50 @@ namespace AsbCloudInfrastructure.Services
if (dto.Id != idWell)
throw new ArgumentInvalidException($"Нельзя поменять id для скважины: {idWell} => {dto.Id}.", nameof(dto));
var entity = Convert(dto);
var oldRelations = await cacheRelationCompaniesWells
.WhereAsync(r => r.IdWell == idWell, token);
var oldRelations = (await GetCacheRelationCompanyWellAsync(token))
.Where(r => r.IdWell == idWell);
if (dto.Companies.Count() != oldRelations.Count() ||
dto.Companies.Any(c => !oldRelations.Any(oldC => oldC.IdCompany == c.Id)))
{
await cacheRelationCompaniesWells.RemoveAsync(r => r.IdWell == idWell, token);
dbContext.RelationCompaniesWells
.RemoveRange(dbContext.RelationCompaniesWells
.Where(r => r.IdWell == idWell));
var newRelations = dto.Companies.Select(c => new RelationCompanyWell { IdWell = idWell, IdCompany = c.Id });
await cacheRelationCompaniesWells.InsertAsync(newRelations, token);
dbContext.RelationCompaniesWells.AddRange(newRelations);
}
var result = await Cache.UpsertAsync(entity, token);
var result = await base.UpdateAsync(idWell, dto, token);
return result;
}
public bool IsCompanyInvolvedInWell(int idCompany, int idWell)
=> cacheRelationCompaniesWells.Contains(r => r.IdWell == idWell && r.IdCompany == idCompany);
=> GetCacheRelationCompanyWell()
.Any(r => r.IdWell == idWell && r.IdCompany == idCompany);
public async Task<bool> IsCompanyInvolvedInWellAsync(int idCompany, int idWell, CancellationToken token)
=> await cacheRelationCompaniesWells.ContainsAsync(r => r.IdWell == idWell &&
r.IdCompany == idCompany, token).ConfigureAwait(false);
=> (await GetCacheRelationCompanyWellAsync(token))
.Any(r => r.IdWell == idWell && r.IdCompany == idCompany);
public async Task<string> GetWellCaptionByIdAsync(int idWell, CancellationToken token)
{
var entity = await Cache.FirstOrDefaultAsync(w => w.Id == idWell, token).ConfigureAwait(false);
var entity = await GetAsync(idWell, token).ConfigureAwait(false);
var dto = Convert(entity);
return dto.Caption;
}
public async Task<IEnumerable<CompanyDto>> GetCompaniesAsync(int idWell, CancellationToken token)
{
var relations = await cacheRelationCompaniesWells.WhereAsync(r => r.IdWell == idWell, token);
var relations = (await GetCacheRelationCompanyWellAsync(token))
.Where(r => r.IdWell == idWell);
var dtos = relations.Select(r => Convert(r.Company));
return dtos;
}
private IEnumerable<CompanyDto> GetCompanies(int idWell)
{
var relations = cacheRelationCompaniesWells.Where(r => r.IdWell == idWell);
var relations = GetCacheRelationCompanyWell().Where(r => r.IdWell == idWell);
var dtos = relations.Select(r => Convert(r.Company));
return dtos;
}
@ -168,16 +200,18 @@ namespace AsbCloudInfrastructure.Services
public async Task<IEnumerable<int>> GetClusterWellsIdsAsync(int idWell, CancellationToken token)
{
var well = await Cache.FirstOrDefaultAsync(w => w.Id == idWell, token)
.ConfigureAwait(false);
var well = await GetAsync(idWell, token);
if (well is null)
return null;
var clusterWells = await Cache.WhereAsync(w => w.IdCluster == well.IdCluster, token)
.ConfigureAwait(false);
var cache = await GetCacheAsync(token);
return clusterWells.Select(w => w.Id);
var clusterWellsIds = cache.Values
.Where((w) => w.IdCluster == well.IdCluster)
.Select(w => w.Id);
return clusterWellsIds;
}
protected override Well Convert(WellDto dto)
@ -187,8 +221,8 @@ namespace AsbCloudInfrastructure.Services
entity.IdTelemetry = entity.IdTelemetry ?? dto.IdTelemetry ?? dto.Telemetry?.Id;
if (dto.Timezone is null)
if (TryGetTimezone(dto.Id, out var timezoneDto))
entity.Timezone = timezoneDto.Adapt<SimpleTimezone>();
entity.Timezone = GetTimezone(dto.Id)
.Adapt<SimpleTimezone>();
return entity;
}
@ -201,15 +235,17 @@ namespace AsbCloudInfrastructure.Services
var dto = base.Convert(entity);
if (entity.Timezone is null)
if (TryGetTimezone(entity, out var timezone))
dto.Timezone = timezone;
dto.Timezone = GetTimezone(entity.Id);
dto.StartDate = wellOperationService.Value.FirstOperationDate(entity.Id)?.ToRemoteDateTime(dto.Timezone.Hours);
dto.StartDate = wellOperationService.FirstOperationDate(entity.Id)?.ToRemoteDateTime(dto.Timezone.Hours);
dto.WellType = entity.WellType?.Caption;
dto.Cluster = entity.Cluster?.Caption;
dto.Deposit = entity.Cluster?.Deposit?.Caption;
dto.LastTelemetryDate = GetLastTelemetryDate(entity.Id).DateTime;
dto.Companies = GetCompanies(entity.Id);
if(entity.IdTelemetry is not null)
dto.LastTelemetryDate = telemetryService.GetLastTelemetryDate((int)entity.IdTelemetry);
dto.Companies = entity.RelationCompaniesWells
.Select(r => Convert(r.Company))
.ToList();
return dto;
}
@ -217,94 +253,56 @@ namespace AsbCloudInfrastructure.Services
{
var dto = entity.Adapt<CompanyDto>();
dto.CompanyTypeCaption = entity.CompanyType?.Caption
?? cacheCompanyWellTypes.FirstOrDefault(c => c.Id == entity.IdCompanyType).Caption;
?? companyTypesService.Get(entity.IdCompanyType).Caption;
return dto;
}
public void EnshureTimezonesIsSet()
public async Task EnshureTimezonesIsSetAsync(CancellationToken token)
{
var wells = Cache.Where(w => w.Timezone is null).ToList();
foreach (var well in wells)
{
if (TryGetTimezone(well, out var timezone))
well.Timezone = timezone.Adapt<SimpleTimezone>();
else
well.Timezone = new SimpleTimezone
{
Hours = 5,
IsOverride = false,
TimeZoneId = "Assumed",
};
}
var cache = await GetCacheAsync(token);
if (!cache.Values.Any(w => w.Timezone is null))
return;
var wellsWithTz = wells.Where(w => w.Timezone is not null);
if (wellsWithTz.Any())
var defaultTimeZone = new SimpleTimezone
{
var adaptedWells = wellsWithTz.Adapt<WellDto>().Select(Convert);
Cache.Upsert(adaptedWells);
}
}
Hours = 5,
IsOverride = false,
TimeZoneId = "Assumed",
};
private bool TryGetTimezone(int idWell, out SimpleTimezoneDto timezone)
{
timezone = null;
try
{
timezone = GetTimezone(idWell);
return timezone is not null;
}
catch
{
return false;
}
await dbSet.Where(w => w.Timezone == null)
.ForEachAsync(w => w.Timezone = defaultTimeZone, token);
await dbContext.SaveChangesAsync(token);
DropCache();
}
public SimpleTimezoneDto GetTimezone(int idWell)
{
var well = Cache.FirstOrDefault(c => c.Id == idWell);
var well = Get(idWell);
if (well == null)
throw new ArgumentInvalidException($"idWell: {idWell} does not exist.", nameof(idWell));
return GetTimezone(well);
}
private bool TryGetTimezone(Well well, out SimpleTimezoneDto timezone)
private SimpleTimezoneDto GetTimezone(WellDto wellDto)
{
timezone = null;
try
{
timezone = GetTimezone(well);
return timezone is not null;
}
catch
{
return false;
}
}
if (wellDto.Timezone is not null)
return wellDto.Timezone;
private SimpleTimezoneDto GetTimezone(Well well)
{
if (well == null)
throw new ArgumentNullException(nameof(well));
if (well.Timezone is not null)
return well.Timezone.Adapt<SimpleTimezoneDto>();
if (well.Telemetry is not null)
if (wellDto.Telemetry is not null)
{
var timezone = telemetryService.GetTimezone(well.Telemetry.Id);
var timezone = telemetryService.GetTimezone(wellDto.Telemetry.Id);
if (timezone is not null)
{
well.Timezone = timezone.Adapt<SimpleTimezone>();
return timezone;
}
}
var well = GetQuery().FirstOrDefault(w => w.Id == wellDto.Id);
var point = GetCoordinates(well);
if (point is not null)
{
if (point.Timezone is not null)
{
well.Timezone = point.Timezone;
return point.Timezone.Adapt<SimpleTimezoneDto>();
}
@ -313,13 +311,12 @@ namespace AsbCloudInfrastructure.Services
var timezone = timezoneService.GetByCoordinates((double)point.Latitude, (double)point.Longitude);
if (timezone is not null)
{
well.Timezone = timezone.Adapt<SimpleTimezone>();
return timezone;
}
}
}
throw new Exception($"Can't find timezone for well {well.Caption} id: {well.Id}");
throw new Exception($"Can't find timezone for well {wellDto.Caption} id: {wellDto.Id}");
}
private static AsbCloudDb.Model.IMapPoint GetCoordinates(Well well)
@ -330,9 +327,9 @@ namespace AsbCloudInfrastructure.Services
if (well.Latitude is not null & well.Longitude is not null)
return well;
if (well.Cluster is null)
if (well.IdCluster is null)
throw new Exception($"Can't find coordinates of well {well.Caption} id: {well.Id}");
var cluster = well.Cluster;
if (cluster.Latitude is not null & cluster.Longitude is not null)
@ -351,7 +348,7 @@ namespace AsbCloudInfrastructure.Services
public DatesRangeDto GetDatesRange(int idWell)
{
var well = Cache.FirstOrDefault(w => w.Id == idWell);
var well = Get(idWell);
if (well is null)
throw new Exception($"Well id: {idWell} does not exist.");

View File

@ -19,7 +19,7 @@ namespace AsbCloudInfrastructure
context.Database.Migrate();
var wellService = scope.ServiceProvider.GetService<IWellService>();
wellService.EnshureTimezonesIsSet();
wellService.EnshureTimezonesIsSetAsync(System.Threading.CancellationToken.None).Wait();
}
}
}

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<ServerGarbageCollection>true</ServerGarbageCollection>
<NoWarn>$(NoWarn);1591</NoWarn>
<UserSecretsId>80899ceb-210f-4f19-ac56-aa90a5d666d4</UserSecretsId>
</PropertyGroup>

View File

@ -12,9 +12,6 @@ namespace AsbCloudWebApi.Controllers
{
public AdminClusterController(ICrudService<ClusterDto> service)
: base(service)
{
service.Includes.Add(nameof(ClusterDto.Wells));
service.Includes.Add(nameof(ClusterDto.Deposit));
}
{}
}
}

View File

@ -13,7 +13,6 @@ namespace AsbCloudWebApi.Controllers
public AdminCompanyController(ICrudService<CompanyDto> service)
: base(service)
{
service.Includes.Add("CompanyType");
}
}
}

View File

@ -13,7 +13,6 @@ namespace AsbCloudWebApi.Controllers
public AdminDepositController(ICrudService<DepositDto> service)
: base(service)
{
service.Includes.Add("Clusters");
}
}
}

View File

@ -18,7 +18,6 @@ namespace AsbCloudWebApi.Controllers
ITelemetryService telemetryService)
: base(service)
{
service.Includes.Add("Well");
this.telemetryService = telemetryService;
}

View File

@ -2,6 +2,8 @@
using AsbCloudApp.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Threading;
using System.Threading.Tasks;
namespace AsbCloudWebApi.Controllers
{
@ -12,15 +14,13 @@ namespace AsbCloudWebApi.Controllers
{
public AdminWellController(IWellService service)
: base(service)
{
service.Includes.Add("Telemetry");
}
{}
[HttpPost("EnshureTimezonesIsSet")]
[Permission]
public IActionResult EnsureTimestamps()
public async Task<IActionResult> EnsureTimestamps(CancellationToken token)
{
((IWellService)service).EnshureTimezonesIsSet();
await ((IWellService)service).EnshureTimezonesIsSetAsync(token);
return Ok();
}
}

View File

@ -91,7 +91,7 @@ namespace AsbCloudWebApi.Controllers
Forbid();
var result = await service.UpdateAsync(id, value, token).ConfigureAwait(false);
if (result == 0)
if (result == ICrudService<T>.ErrorIdNotFound)
return BadRequest($"id:{id} does not exist in the db");
return Ok(result);
}

View File

@ -1,50 +1,50 @@
{
"files": {
"main.css": "/static/css/main.c2a82e71.chunk.css",
"main.js": "/static/js/main.1a56c0e0.chunk.js",
"main.js.map": "/static/js/main.1a56c0e0.chunk.js.map",
"runtime-main.js": "/static/js/runtime-main.8da12c69.js",
"runtime-main.js.map": "/static/js/runtime-main.8da12c69.js.map",
"static/js/2.f196b75b.chunk.js": "/static/js/2.f196b75b.chunk.js",
"static/js/2.f196b75b.chunk.js.map": "/static/js/2.f196b75b.chunk.js.map",
"main.css": "/static/css/main.a0664ea6.chunk.css",
"main.js": "/static/js/main.02d15bac.chunk.js",
"main.js.map": "/static/js/main.02d15bac.chunk.js.map",
"runtime-main.js": "/static/js/runtime-main.83ebcb38.js",
"runtime-main.js.map": "/static/js/runtime-main.83ebcb38.js.map",
"static/js/2.ebe1f792.chunk.js": "/static/js/2.ebe1f792.chunk.js",
"static/js/2.ebe1f792.chunk.js.map": "/static/js/2.ebe1f792.chunk.js.map",
"static/css/3.f8ac3883.chunk.css": "/static/css/3.f8ac3883.chunk.css",
"static/js/3.31b66021.chunk.js": "/static/js/3.31b66021.chunk.js",
"static/js/3.31b66021.chunk.js.map": "/static/js/3.31b66021.chunk.js.map",
"static/js/3.a763380a.chunk.js": "/static/js/3.a763380a.chunk.js",
"static/js/3.a763380a.chunk.js.map": "/static/js/3.a763380a.chunk.js.map",
"static/css/4.f8ac3883.chunk.css": "/static/css/4.f8ac3883.chunk.css",
"static/js/4.1f09e89e.chunk.js": "/static/js/4.1f09e89e.chunk.js",
"static/js/4.1f09e89e.chunk.js.map": "/static/js/4.1f09e89e.chunk.js.map",
"static/js/5.ef929bfe.chunk.js": "/static/js/5.ef929bfe.chunk.js",
"static/js/5.ef929bfe.chunk.js.map": "/static/js/5.ef929bfe.chunk.js.map",
"static/js/6.88051835.chunk.js": "/static/js/6.88051835.chunk.js",
"static/js/6.88051835.chunk.js.map": "/static/js/6.88051835.chunk.js.map",
"static/js/7.4f3c315a.chunk.js": "/static/js/7.4f3c315a.chunk.js",
"static/js/7.4f3c315a.chunk.js.map": "/static/js/7.4f3c315a.chunk.js.map",
"static/js/8.8e9a1dc7.chunk.js": "/static/js/8.8e9a1dc7.chunk.js",
"static/js/8.8e9a1dc7.chunk.js.map": "/static/js/8.8e9a1dc7.chunk.js.map",
"static/js/9.71667cac.chunk.js": "/static/js/9.71667cac.chunk.js",
"static/js/9.71667cac.chunk.js.map": "/static/js/9.71667cac.chunk.js.map",
"static/js/10.e5247b1b.chunk.js": "/static/js/10.e5247b1b.chunk.js",
"static/js/10.e5247b1b.chunk.js.map": "/static/js/10.e5247b1b.chunk.js.map",
"static/js/11.70112c8f.chunk.js": "/static/js/11.70112c8f.chunk.js",
"static/js/11.70112c8f.chunk.js.map": "/static/js/11.70112c8f.chunk.js.map",
"static/js/12.2265b74f.chunk.js": "/static/js/12.2265b74f.chunk.js",
"static/js/12.2265b74f.chunk.js.map": "/static/js/12.2265b74f.chunk.js.map",
"static/js/13.063a16c9.chunk.js": "/static/js/13.063a16c9.chunk.js",
"static/js/13.063a16c9.chunk.js.map": "/static/js/13.063a16c9.chunk.js.map",
"static/js/14.50a284b1.chunk.js": "/static/js/14.50a284b1.chunk.js",
"static/js/14.50a284b1.chunk.js.map": "/static/js/14.50a284b1.chunk.js.map",
"static/js/4.14deb3a9.chunk.js": "/static/js/4.14deb3a9.chunk.js",
"static/js/4.14deb3a9.chunk.js.map": "/static/js/4.14deb3a9.chunk.js.map",
"static/js/5.54daf1dd.chunk.js": "/static/js/5.54daf1dd.chunk.js",
"static/js/5.54daf1dd.chunk.js.map": "/static/js/5.54daf1dd.chunk.js.map",
"static/js/6.2f64a277.chunk.js": "/static/js/6.2f64a277.chunk.js",
"static/js/6.2f64a277.chunk.js.map": "/static/js/6.2f64a277.chunk.js.map",
"static/js/7.8c90cea1.chunk.js": "/static/js/7.8c90cea1.chunk.js",
"static/js/7.8c90cea1.chunk.js.map": "/static/js/7.8c90cea1.chunk.js.map",
"static/js/8.6e937634.chunk.js": "/static/js/8.6e937634.chunk.js",
"static/js/8.6e937634.chunk.js.map": "/static/js/8.6e937634.chunk.js.map",
"static/js/9.3b35991a.chunk.js": "/static/js/9.3b35991a.chunk.js",
"static/js/9.3b35991a.chunk.js.map": "/static/js/9.3b35991a.chunk.js.map",
"static/js/10.69527c71.chunk.js": "/static/js/10.69527c71.chunk.js",
"static/js/10.69527c71.chunk.js.map": "/static/js/10.69527c71.chunk.js.map",
"static/js/11.f8320c6a.chunk.js": "/static/js/11.f8320c6a.chunk.js",
"static/js/11.f8320c6a.chunk.js.map": "/static/js/11.f8320c6a.chunk.js.map",
"static/js/12.7a9654fd.chunk.js": "/static/js/12.7a9654fd.chunk.js",
"static/js/12.7a9654fd.chunk.js.map": "/static/js/12.7a9654fd.chunk.js.map",
"static/js/13.35247644.chunk.js": "/static/js/13.35247644.chunk.js",
"static/js/13.35247644.chunk.js.map": "/static/js/13.35247644.chunk.js.map",
"static/js/14.0f147158.chunk.js": "/static/js/14.0f147158.chunk.js",
"static/js/14.0f147158.chunk.js.map": "/static/js/14.0f147158.chunk.js.map",
"index.html": "/index.html",
"static/css/3.f8ac3883.chunk.css.map": "/static/css/3.f8ac3883.chunk.css.map",
"static/css/4.f8ac3883.chunk.css.map": "/static/css/4.f8ac3883.chunk.css.map",
"static/css/main.c2a82e71.chunk.css.map": "/static/css/main.c2a82e71.chunk.css.map",
"static/js/2.f196b75b.chunk.js.LICENSE.txt": "/static/js/2.f196b75b.chunk.js.LICENSE.txt",
"static/css/main.a0664ea6.chunk.css.map": "/static/css/main.a0664ea6.chunk.css.map",
"static/js/2.ebe1f792.chunk.js.LICENSE.txt": "/static/js/2.ebe1f792.chunk.js.LICENSE.txt",
"static/media/ClusterIcon.f85713df.svg": "/static/media/ClusterIcon.f85713df.svg",
"static/media/DepositIcon.9688e406.svg": "/static/media/DepositIcon.9688e406.svg"
},
"entrypoints": [
"static/js/runtime-main.8da12c69.js",
"static/js/2.f196b75b.chunk.js",
"static/css/main.c2a82e71.chunk.css",
"static/js/main.1a56c0e0.chunk.js"
"static/js/runtime-main.83ebcb38.js",
"static/js/2.ebe1f792.chunk.js",
"static/css/main.a0664ea6.chunk.css",
"static/js/main.02d15bac.chunk.js"
]
}

View File

@ -1 +1 @@
<!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="Онлайн мониторинг процесса бурения в реальном времени в офисе заказчика"/><link rel="manifest" href="/manifest.json"/><title>АСБ Vision</title><link href="/static/css/main.c2a82e71.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(e){function t(t){for(var n,o,u=t[0],f=t[1],i=t[2],l=0,d=[];l<u.length;l++)o=u[l],Object.prototype.hasOwnProperty.call(a,o)&&a[o]&&d.push(a[o][0]),a[o]=0;for(n in f)Object.prototype.hasOwnProperty.call(f,n)&&(e[n]=f[n]);for(s&&s(t);d.length;)d.shift()();return c.push.apply(c,i||[]),r()}function r(){for(var e,t=0;t<c.length;t++){for(var r=c[t],n=!0,o=1;o<r.length;o++){var f=r[o];0!==a[f]&&(n=!1)}n&&(c.splice(t--,1),e=u(u.s=r[0]))}return e}var n={},o={1:0},a={1:0},c=[];function u(t){if(n[t])return n[t].exports;var r=n[t]={i:t,l:!1,exports:{}};return e[t].call(r.exports,r,r.exports,u),r.l=!0,r.exports}u.e=function(e){var t=[];o[e]?t.push(o[e]):0!==o[e]&&{3:1,4:1}[e]&&t.push(o[e]=new Promise((function(t,r){for(var n="static/css/"+({}[e]||e)+"."+{3:"f8ac3883",4:"f8ac3883",5:"31d6cfe0",6:"31d6cfe0",7:"31d6cfe0",8:"31d6cfe0",9:"31d6cfe0",10:"31d6cfe0",11:"31d6cfe0",12:"31d6cfe0",13:"31d6cfe0",14:"31d6cfe0"}[e]+".chunk.css",a=u.p+n,c=document.getElementsByTagName("link"),f=0;f<c.length;f++){var i=(s=c[f]).getAttribute("data-href")||s.getAttribute("href");if("stylesheet"===s.rel&&(i===n||i===a))return t()}var l=document.getElementsByTagName("style");for(f=0;f<l.length;f++){var s;if((i=(s=l[f]).getAttribute("data-href"))===n||i===a)return t()}var d=document.createElement("link");d.rel="stylesheet",d.type="text/css",d.onload=t,d.onerror=function(t){var n=t&&t.target&&t.target.src||a,c=new Error("Loading CSS chunk "+e+" failed.\n("+n+")");c.code="CSS_CHUNK_LOAD_FAILED",c.request=n,delete o[e],d.parentNode.removeChild(d),r(c)},d.href=a,document.getElementsByTagName("head")[0].appendChild(d)})).then((function(){o[e]=0})));var r=a[e];if(0!==r)if(r)t.push(r[2]);else{var n=new Promise((function(t,n){r=a[e]=[t,n]}));t.push(r[2]=n);var c,f=document.createElement("script");f.charset="utf-8",f.timeout=120,u.nc&&f.setAttribute("nonce",u.nc),f.src=function(e){return u.p+"static/js/"+({}[e]||e)+"."+{3:"31b66021",4:"1f09e89e",5:"ef929bfe",6:"88051835",7:"4f3c315a",8:"8e9a1dc7",9:"71667cac",10:"e5247b1b",11:"70112c8f",12:"2265b74f",13:"063a16c9",14:"50a284b1"}[e]+".chunk.js"}(e);var i=new Error;c=function(t){f.onerror=f.onload=null,clearTimeout(l);var r=a[e];if(0!==r){if(r){var n=t&&("load"===t.type?"missing":t.type),o=t&&t.target&&t.target.src;i.message="Loading chunk "+e+" failed.\n("+n+": "+o+")",i.name="ChunkLoadError",i.type=n,i.request=o,r[1](i)}a[e]=void 0}};var l=setTimeout((function(){c({type:"timeout",target:f})}),12e4);f.onerror=f.onload=c,document.head.appendChild(f)}return Promise.all(t)},u.m=e,u.c=n,u.d=function(e,t,r){u.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},u.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},u.t=function(e,t){if(1&t&&(e=u(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(u.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)u.d(r,n,function(t){return e[t]}.bind(null,n));return r},u.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return u.d(t,"a",t),t},u.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},u.p="/",u.oe=function(e){throw console.error(e),e};var f=this.webpackJsonpasb_cloud_front_react=this.webpackJsonpasb_cloud_front_react||[],i=f.push.bind(f);f.push=t,f=f.slice();for(var l=0;l<f.length;l++)t(f[l]);var s=i;r()}([])</script><script src="/static/js/2.f196b75b.chunk.js"></script><script src="/static/js/main.1a56c0e0.chunk.js"></script></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="Онлайн мониторинг процесса бурения в реальном времени в офисе заказчика"/><link rel="manifest" href="/manifest.json"/><title>АСБ Vision</title><link href="/static/css/main.a0664ea6.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(e){function t(t){for(var n,o,u=t[0],f=t[1],i=t[2],l=0,d=[];l<u.length;l++)o=u[l],Object.prototype.hasOwnProperty.call(a,o)&&a[o]&&d.push(a[o][0]),a[o]=0;for(n in f)Object.prototype.hasOwnProperty.call(f,n)&&(e[n]=f[n]);for(s&&s(t);d.length;)d.shift()();return c.push.apply(c,i||[]),r()}function r(){for(var e,t=0;t<c.length;t++){for(var r=c[t],n=!0,o=1;o<r.length;o++){var f=r[o];0!==a[f]&&(n=!1)}n&&(c.splice(t--,1),e=u(u.s=r[0]))}return e}var n={},o={1:0},a={1:0},c=[];function u(t){if(n[t])return n[t].exports;var r=n[t]={i:t,l:!1,exports:{}};return e[t].call(r.exports,r,r.exports,u),r.l=!0,r.exports}u.e=function(e){var t=[];o[e]?t.push(o[e]):0!==o[e]&&{3:1,4:1}[e]&&t.push(o[e]=new Promise((function(t,r){for(var n="static/css/"+({}[e]||e)+"."+{3:"f8ac3883",4:"f8ac3883",5:"31d6cfe0",6:"31d6cfe0",7:"31d6cfe0",8:"31d6cfe0",9:"31d6cfe0",10:"31d6cfe0",11:"31d6cfe0",12:"31d6cfe0",13:"31d6cfe0",14:"31d6cfe0"}[e]+".chunk.css",a=u.p+n,c=document.getElementsByTagName("link"),f=0;f<c.length;f++){var i=(s=c[f]).getAttribute("data-href")||s.getAttribute("href");if("stylesheet"===s.rel&&(i===n||i===a))return t()}var l=document.getElementsByTagName("style");for(f=0;f<l.length;f++){var s;if((i=(s=l[f]).getAttribute("data-href"))===n||i===a)return t()}var d=document.createElement("link");d.rel="stylesheet",d.type="text/css",d.onload=t,d.onerror=function(t){var n=t&&t.target&&t.target.src||a,c=new Error("Loading CSS chunk "+e+" failed.\n("+n+")");c.code="CSS_CHUNK_LOAD_FAILED",c.request=n,delete o[e],d.parentNode.removeChild(d),r(c)},d.href=a,document.getElementsByTagName("head")[0].appendChild(d)})).then((function(){o[e]=0})));var r=a[e];if(0!==r)if(r)t.push(r[2]);else{var n=new Promise((function(t,n){r=a[e]=[t,n]}));t.push(r[2]=n);var c,f=document.createElement("script");f.charset="utf-8",f.timeout=120,u.nc&&f.setAttribute("nonce",u.nc),f.src=function(e){return u.p+"static/js/"+({}[e]||e)+"."+{3:"a763380a",4:"14deb3a9",5:"54daf1dd",6:"2f64a277",7:"8c90cea1",8:"6e937634",9:"3b35991a",10:"69527c71",11:"f8320c6a",12:"7a9654fd",13:"35247644",14:"0f147158"}[e]+".chunk.js"}(e);var i=new Error;c=function(t){f.onerror=f.onload=null,clearTimeout(l);var r=a[e];if(0!==r){if(r){var n=t&&("load"===t.type?"missing":t.type),o=t&&t.target&&t.target.src;i.message="Loading chunk "+e+" failed.\n("+n+": "+o+")",i.name="ChunkLoadError",i.type=n,i.request=o,r[1](i)}a[e]=void 0}};var l=setTimeout((function(){c({type:"timeout",target:f})}),12e4);f.onerror=f.onload=c,document.head.appendChild(f)}return Promise.all(t)},u.m=e,u.c=n,u.d=function(e,t,r){u.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},u.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},u.t=function(e,t){if(1&t&&(e=u(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(u.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)u.d(r,n,function(t){return e[t]}.bind(null,n));return r},u.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return u.d(t,"a",t),t},u.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},u.p="/",u.oe=function(e){throw console.error(e),e};var f=this.webpackJsonpasb_cloud_front_react=this.webpackJsonpasb_cloud_front_react||[],i=f.push.bind(f);f.push=t,f=f.slice();for(var l=0;l<f.length;l++)t(f[l]);var s=i;r()}([])</script><script src="/static/js/2.ebe1f792.chunk.js"></script><script src="/static/js/main.02d15bac.chunk.js"></script></body></html>