Files
lux/EgwCoreLib.Lux.Data/Services/DataLayerServices.cs
T
2026-03-20 10:41:52 +01:00

626 lines
27 KiB
C#

using EgwCoreLib.Lux.Core.Generic;
using EgwCoreLib.Lux.Core.RestPayload;
using EgwCoreLib.Lux.Data.Controllers;
using EgwCoreLib.Lux.Data.DbModel.Job;
using EgwCoreLib.Lux.Data.DbModel.Production;
using EgwCoreLib.Lux.Data.DbModel.Sales;
using EgwCoreLib.Lux.Data.Services.Config;
using EgwCoreLib.Lux.Data.Services.Production;
using EgwCoreLib.Lux.Data.Services.Sales;
using EgwMultiEngineManager.Data;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using NLog;
using StackExchange.Redis;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
using static EgwCoreLib.Lux.Core.Enums;
namespace EgwCoreLib.Lux.Data.Services
{
public class DataLayerServices : BaseServ
{
#region Public Constructors
public DataLayerServices(
IConfiguration configuration,
IConnectionMultiplexer RedisConn,
IServiceProvider serviceProvider) : base(configuration, RedisConn)
{
_serviceProvider = serviceProvider;
// conf DB
string connStr = _config.GetConnectionString("Lux.All") ?? "";
if (string.IsNullOrEmpty(connStr))
{
Log.Error("ConnString empty!");
}
else
{
StringBuilder sb = new StringBuilder();
//dbController = new Controllers.LuxController(_config);
dbController = new LuxController();
sb.AppendLine($"LuxController OK");
dataSimController = new DataSimulatorController();
sb.AppendLine($"DataSimController OK");
sb.AppendLine($"LuxController OK");
Log.Info(sb.ToString());
}
}
#endregion Public Constructors
#region Public Properties
public IConfProfileService ConfProfServ => _serviceProvider.GetRequiredService<IConfProfileService>();
public IOfferRowService OfferRowServ => _serviceProvider.GetRequiredService<IOfferRowService>();
public IOrderRowService OrderRowServ => _serviceProvider.GetRequiredService<IOrderRowService>();
public IProductionGroupService PrGrpServ => _serviceProvider.GetRequiredService<IProductionGroupService>();
public IProductionOdlService PrOdlServ => _serviceProvider.GetRequiredService<IProductionOdlService>();
#endregion Public Properties
#region Public Methods
/// <summary>
/// Sistema gli item che mancassero di ItemID leggendo da DB
/// </summary>
/// <param name="listOrg"></param>
/// <returns></returns>
public List<BomItemDTO> BomFixItemId(List<BomItemDTO> listOrg)
{
using var activity = StartActivity();
string source = "DB";
List<BomItemDTO> listFix = listOrg;
// sistemo ItemID per gli item della BOM...
var listItemDB = dbController.ItemGetFilt("", ItemClassType.Bom);
// cerco i record da sistemare ed 1:1 li fixo...
foreach (var item in listFix.Where(x => x.ItemID == 0))
{
var dbRec = listItemDB.FirstOrDefault(x => x.ExtItemCode == item.ItemCode);
if ((dbRec != null))
{
item.ItemID = dbRec.ItemID;
}
}
activity?.SetTag("data.source", source);
LogTrace($"{source} | trace: {activity?.TraceId} | {activity?.Duration.TotalMilliseconds}ms");
return listFix;
}
/// <summary>
/// Reset completo cache sistema
/// </summary>
public bool FlushCache()
{
using var activity = StartActivity();
string source = "REDIS";
bool answ = false;
RedisValue pattern = new RedisValue($"{redisBaseKey}:*");
answ = ExecFlushRedisPattern(pattern);
activity?.SetTag("data.source", source);
LogTrace($"{source} | trace: {activity?.TraceId} | {activity?.Duration.TotalMilliseconds}ms");
return answ;
}
/// <summary>
/// Reset completo cache sistema modalità async
/// </summary>
public async Task<bool> FlushCacheAsync()
{
using var activity = StartActivity();
string source = "REDIS";
bool answ = false;
RedisValue pattern = new RedisValue($"{redisBaseKey}:*");
answ = await ExecFlushRedisPatternAsync(pattern);
activity?.SetTag("data.source", source);
LogTrace($"{source} | trace: {activity?.TraceId} | {activity?.Duration.TotalMilliseconds}ms");
return answ;
}
/// <summary>
/// Reset cache sistema x Offerte modalità async
/// </summary>
public async Task<bool> FlushCacheOffersAsync()
{
using var activity = StartActivity();
string source = "REDIS";
bool answ = false;
await ExecFlushRedisPatternAsync((RedisValue)$"{redisBaseKey}:Offers:*");
await ExecFlushRedisPatternAsync((RedisValue)$"{redisBaseKey}:OfferRows:*");
activity?.SetTag("data.source", source);
LogTrace($"{source} | trace: {activity?.TraceId} | {activity?.Duration.TotalMilliseconds}ms");
return answ;
}
/// <summary>
/// Reset cache sistema x Ordini modalità async
/// </summary>
public async Task<bool> FlushCacheOrdersAsync()
{
using var activity = StartActivity();
string source = "REDIS";
bool answ = false;
await ExecFlushRedisPatternAsync((RedisValue)$"{redisBaseKey}:Orders:*");
await ExecFlushRedisPatternAsync((RedisValue)$"{redisBaseKey}:OrderRows:*");
activity?.SetTag("data.source", source);
LogTrace($"{source} | trace: {activity?.TraceId} | {activity?.Duration.TotalMilliseconds}ms");
return answ;
}
/// <summary>
/// Verifica offerte scadute, con update sul DB
/// </summary>
public async Task<bool> OffersCheckExpired()
{
using var activity = StartActivity();
string source = "DB+REDIS";
// calcolo
bool fatto = await dbController.OffersCheckExpired();
// svuoto cache...
await ExecFlushRedisPatternAsync((RedisValue)$"{redisBaseKey}:Offers:*");
await ExecFlushRedisPatternAsync((RedisValue)$"{redisBaseKey}:OfferRows:*");
activity?.SetTag("data.source", source);
LogTrace($"{source} | trace: {activity?.TraceId} | {activity?.Duration.TotalMilliseconds}ms");
return fatto;
}
/// <summary>
/// Converte il campo raw della BOM in lista oggetti da gestire
/// </summary>
/// <param name="currRec"></param>
/// <returns></returns>
public List<BomItemDTO> OrderGetBomList(OrderRowModel currRec)
{
List<BomItemDTO> answ = new List<BomItemDTO>();
var bomList = JsonConvert.DeserializeObject<List<BomItemDTO>>(currRec.ItemBOM);
if (bomList != null)
{
answ = bomList;
}
return answ;
}
/// <summary>
/// Elenco completo Fasi
/// </summary>
/// <returns></returns>
public async Task<List<PhaseModel>> PhasesGetAllAsync()
{
using var activity = StartActivity();
string source = "DB";
List<PhaseModel>? result = new List<PhaseModel>();
// cerco in redis...
string currKey = $"{redisBaseKey}:Phases:ALL";
RedisValue rawData = await _redisDb.StringGetAsync(currKey);
if (rawData.HasValue)
{
result = JsonConvert.DeserializeObject<List<PhaseModel>>($"{rawData}");
source = "REDIS";
}
else
{
result = await dbController.PhasesGetAllAsync();
// serializzo e salvo con config x evitare loop...
rawData = JsonConvert.SerializeObject(result, JSSettings);
await _redisDb.StringSetAsync(currKey, rawData, LongCache);
}
if (result == null)
{
result = new List<PhaseModel>();
}
activity?.SetTag("data.source", source);
LogTrace($"{source} | trace: {activity?.TraceId} | {activity?.Duration.TotalMilliseconds}ms");
return result;
}
/// <summary>
/// Restituisce un dizionario di <CodFase,EventDto> da usare nel planner
/// </summary>
/// <param name="dtStart"></param>
/// <param name="dtEnd"></param>
/// <returns></returns>
public async Task<Dictionary<string, List<EventDto>>?> PlannerGetEvents(DateTime dtStart, DateTime dtEnd)
{
using var activity = StartActivity();
string source = "DB";
Dictionary<string, List<EventDto>>? result = null;
// cerco in redis...
string currKey = $"{redisBaseKey}:PlannerData:{dtStart:yyyyMMdd}:{dtEnd:yyyyMMdd}";
RedisValue rawData = await _redisDb.StringGetAsync(currKey);
//if (!string.IsNullOrEmpty($"{rawData}"))
if (rawData.HasValue && rawData.Length() > 2)
{
result = JsonConvert.DeserializeObject<Dictionary<string, List<EventDto>>>($"{rawData}");
source = "REDIS";
}
else
{
result = dataSimController.PlannerGetEvents(dtStart, dtEnd);
// serializzo e salvo...
rawData = JsonConvert.SerializeObject(result);
await _redisDb.StringSetAsync(currKey, rawData, LongCache);
}
activity?.SetTag("data.source", source);
LogTrace($"{source} | trace: {activity?.TraceId} | {activity?.Duration.TotalMilliseconds}ms");
return result;
}
/// <summary>
/// Assegnazione in blocco degli item agli ODL corrispondenti
/// </summary>
/// <param name="dbList"></param>
/// <param name="dictParts"></param>
/// <returns></returns>
public async Task<int> ProdItem2ODL_AssignAsync(List<ProductionODLModel> dbList, Dictionary<(int phaseId, int resId, string machine, int index), List<string>> dictParts)
{
using var activity = StartActivity();
string source = "DB+REDIS";
// calcolo
int fatto = await dbController.ProdItem2ODL_AssignAsync(dbList, dictParts);
// svuoto cache...
//await ExecFlushRedisPatternAsync((RedisValue)$"{_redisBaseKey}:ProdItems:*");
await ExecFlushRedisPatternAsync((RedisValue)$"{redisBaseKey}:ProdItems:OrdRowId:*");
await ExecFlushRedisPatternAsync((RedisValue)$"{redisBaseKey}:ProdGroup:OrdRowId:*");
await ExecFlushRedisPatternAsync((RedisValue)$"{redisBaseKey}:OrderRows:*");
await ExecFlushRedisPatternAsync((RedisValue)$"{redisBaseKey}:OrderRowsByState:*");
activity?.SetTag("data.source", source);
LogTrace($"{source} | trace: {activity?.TraceId} | {activity?.Duration.TotalMilliseconds}ms");
return fatto;
}
/// <summary>
/// Esegue salvataggio BOM sul DB
/// </summary>
/// <param name="uID">UID dell'item offerta di cui si è ricevuto la BOM</param>
/// <param name="qMode">Mode della chiamata (x decidere cosa/dove salvare)</param>
/// <param name="execEnvironment">Environment dell'item</param>
/// <param name="bomContent">BOM serializzata</param>
/// <returns></returns>
public async Task SaveBomAsync(string uID, Egw.Window.Data.Enums.QuestionModes qMode, Constants.EXECENVIRONMENTS execEnvironment, string bomContent)
{
using var activity = StartActivity();
string source = "DB";
// salvo sul DB il risultato della BOM
if (!string.IsNullOrEmpty(bomContent))
{
List<BomItemDTO>? bomList = null;
try
{
// deserializzo la Bom...
bomList = JsonConvert.DeserializeObject<List<BomItemDTO>>(bomContent);
}
catch { }
if (bomList != null)
{
// verifico 1:1 gli item ricevuti dalla BOM sul DB con eventuale insert in anagrafica dei nuovi
dbController.ItemUpsertFromBom(bomList);
// se mode = 2 è offerta, se 5 = ODL...
switch (qMode)
{
case Egw.Window.Data.Enums.QuestionModes.NULL:
break;
case Egw.Window.Data.Enums.QuestionModes.PREVIEW:
break;
// BOM in offerta
case Egw.Window.Data.Enums.QuestionModes.BOM:
// salvo la BOM nel record Offer del DB relativo all'oggetto richiesto
await OfferRowServ.UpdateBomAsync(uID, bomList);
break;
case Egw.Window.Data.Enums.QuestionModes.HARDWARE:
break;
case Egw.Window.Data.Enums.QuestionModes.CONFIG:
break;
// BOM in ODL
case Egw.Window.Data.Enums.QuestionModes.ORDER:
// salvo la BOM nel record ProdODL del DB relativo all'oggetto richiesto
await PrOdlServ.UpdateBomAsync(uID, bomContent);
// salvo in cache BOM aggiornata...
string currKey = $"{redisBaseKey}:ProdOdl:{uID}";
await _redisDb.StringSetAsync(currKey, bomContent, LongCache);
break;
default:
break;
}
}
}
activity?.SetTag("data.source", source);
LogTrace($"{source} | trace: {activity?.TraceId} | {activity?.Duration.TotalMilliseconds}ms");
}
/// <summary>
/// Esegue salvataggio HardwareModelList sul DB
/// </summary>
/// <param name="uID">UID dell'item offerta di cui si è ricevuto la BOM</param>
/// <param name="execEnvironment">Environment dell'item</param>
/// <param name="rawContent">HardwareModelList serializzata</param>
/// <returns></returns>
public async Task SaveHmlAsync(string uID, Constants.EXECENVIRONMENTS execEnvironment, string rawContent)
{
// non effettuo salvataggio sul DB perché master del dato è esterno e lo lasceremo solo in cache REDIS
await Task.Delay(1);
using var activity = StartActivity();
string source = "NONE";
activity?.SetTag("data.source", source);
LogTrace($"{source} | trace: {activity?.TraceId} | {activity?.Duration.TotalMilliseconds}ms");
}
/// <summary>
/// Esegue salvataggio ProdBalance (di un gruppo lavorazioni per riga ordine) sul DB
/// </summary>
/// <param name="uID">UID dell'item offerta di cui si è ricevuto l'oggetto Balance'</param>
/// <param name="rGroup">Prod Group di riferimento</param>
/// <param name="execEnvironment">Environment dell'item</param>
/// <param name="balance">Stima Balance serializzata</param>
/// <returns></returns>
public async Task<bool> SaveProdBalanceAsync(string uID, string rGroup, Constants.EXECENVIRONMENTS execEnvironment, string balance)
{
var result = await PrGrpServ.UpsertBalanceAsync(uID, rGroup, balance);
return result;
}
/// <summary>
/// Esegue salvataggio ProdEstimate (per riga ordine) sul DB
/// </summary>
/// <param name="uID">UID dell'item offerta di cui si è ricevuto la ProdEstim</param>
/// <param name="execEnvironment">Environment dell'item</param>
/// <param name="prodEstim">Stima ProdEstimate serializzata</param>
/// <returns></returns>
public async Task<bool> SaveProdEstimateAsync(string uID, Constants.EXECENVIRONMENTS execEnvironment, string prodEstim)
{
var result = await OrderRowServ.SaveProdEstAsync(uID, prodEstim);
return result;
}
/// <summary>
/// MockUp (fake) salvataggio ProfileList sul DB
/// </summary>
/// <param name="uID">UID ricezione (default tipicamente)</param>
/// <param name="execEnvironment">Environment dell'item</param>
/// <param name="rawContent">ProfileList serializzata</param>
/// <returns></returns>
public async Task<bool> SaveProfileListAsync(string uID, Constants.EXECENVIRONMENTS execEnvironment, string rawContent)
{
var result = await ConfProfServ.SaveProfileListAsync(uID, execEnvironment, rawContent);
return result;
}
/// <summary>
/// MockUp (fake) salvataggio ProfileThreshold sul DB
/// </summary>
/// <param name="uID">UID del profilo</param>
/// <param name="execEnvironment">Environment dell'item</param>
/// <param name="rawThreshold">ThresholdList serializzata</param>
/// <param name="rawData">Info profile data serializzati</param>
/// <returns></returns>
public async Task<bool> SaveProfileThreshAsync(string uID, Constants.EXECENVIRONMENTS execEnvironment, string rawThreshold, string rawData)
{
var result = await ConfProfServ.SaveProfileThreshAsync(uID, execEnvironment, rawThreshold, rawData);
return result;
}
#endregion Public Methods
#region Protected Fields
/// <summary>
/// Numero di operazioni parallele che si possono svolgere... (se 0 NON usa cicli paralleli)
/// </summary>
protected int numPar = 0;
#endregion Protected Fields
#region Protected Methods
/// <summary>
/// Helper avvio attività per la funzione tracciata
/// </summary>
/// <param name="methodName"></param>
/// <returns></returns>
protected new static Activity? StartActivity([CallerMemberName] string? methodName = null)
{
var activity = ActivitySource.StartActivity(methodName ?? "UNDEF");
activity?.SetTag("host.name", Environment.MachineName);
return activity;
}
/// <summary>
/// Esegue flush memoria redis dato pat2Flush
/// </summary>
/// <param name="pat2Flush"></param>
/// <returns></returns>
protected bool ExecFlushRedisPattern(RedisValue pat2Flush)
{
bool answ = false;
using var activity = StartActivity();
string source = "REDIS";
/*******************************
* Recupero elenco dei server da cui cancellare le chiavi:
* - prendo solo i server connessi
* - prendo solo NON repliche (= master)
* - me ne aspetto 1 in uscita cmq
*******************************/
var connServ = _redisConn.GetEndPoints()
.Select(endpoint =>
{
var server = _redisConn.GetServer(endpoint);
return server;
})
.Where(x => x.IsConnected && !x.IsReplica)
.FirstOrDefault();
if (connServ != null)
{
try
{
// sepattern è "*" elimino intero DB...
if ((pat2Flush.Equals(new RedisValue("*")) || pat2Flush == RedisValue.Null))
{
connServ.FlushDatabase(database: _redisDb.Database);
}
else
{
var keys = connServ.Keys(database: _redisDb.Database, pattern: pat2Flush, pageSize: 1000);
var batch = new List<RedisKey>();
foreach (var key in keys)
{
batch.Add(key);
// Flush in batches of 1000
if (batch.Count >= 1000)
{
foreach (var item in batch)
_redisDb.KeyDelete(item);
batch.Clear();
}
}
// Flush remaining keys
foreach (var item in batch)
_redisDb.KeyDelete(item);
}
answ = true;
}
catch (Exception exc)
{
string exMsg = $"Eccezione durante ExecFlushRedisPattern | pat2Flush: {pat2Flush}";
LogTrace($"{exMsg}{Environment.NewLine}{exc}", LogLevel.Error);
// traccio errore
activity?.SetStatus(ActivityStatusCode.Error, exc.Message);
activity?.AddEvent(new ActivityEvent("exception", tags: new ActivityTagsCollection {
{ "exception.type", exc.GetType().Name },
{ "exception.message", exc.Message },
{ "exception.stacktrace", exc.StackTrace }
}));
}
}
else
{
LogTrace($"Server REDIS master non trovato", LogLevel.Error);
}
// aggiunta tags + log
activity?.SetTag("data.source", source);
activity?.SetTag("param.pat2Flush", pat2Flush);
LogTrace($"{pat2Flush} | {source} | trace: {activity?.TraceId} | {activity?.Duration.TotalMilliseconds}ms");
return answ;
}
/// <summary>
/// Esegue flush memoria redis dato pat2Flush
/// </summary>
/// <param name="pat2Flush"></param>
/// <returns></returns>
protected async Task<bool> ExecFlushRedisPatternAsync(RedisValue pat2Flush)
{
bool answ = false;
using var activity = StartActivity();
string source = "REDIS";
/*******************************
* Recupero elenco dei server da cui cancellare le chaivi:
* - prendo solo i server connessi
* - prendo solo NON repliche (= master)
* - me ne aspetto 1 in uscita / prendo primo o default
*******************************/
var connServ = _redisConn.GetEndPoints()
.Select(endpoint =>
{
var server = _redisConn.GetServer(endpoint);
return server;
})
.Where(x => x.IsConnected && !x.IsReplica)
.FirstOrDefault();
if (connServ != null)
{
// ciclo (anche se me ne aspetto 1 solo)
// se pat2Flush è "*" elimino intero DB...
if ((pat2Flush.Equals(new RedisValue("*")) || pat2Flush == RedisValue.Null))
{
connServ.FlushDatabase(database: _redisDb.Database);
}
else
{
try
{
var keys = connServ.Keys(database: _redisDb.Database, pattern: pat2Flush, pageSize: 1000);
var deleteTasks = new List<Task>();
foreach (var key in keys)
{
deleteTasks.Add(_redisDb.KeyDeleteAsync(key));
if (deleteTasks.Count >= 1000)
{
await Task.WhenAll(deleteTasks);
deleteTasks.Clear();
}
}
if (deleteTasks.Count > 0)
{
await Task.WhenAll(deleteTasks);
}
answ = true;
}
catch (Exception exc)
{
string exMsg = $"Eccezione durante ExecFlushRedisPatternAsync | pat2Flush: {pat2Flush}";
LogTrace($"{exMsg}{Environment.NewLine}{exc}", LogLevel.Error);
// traccio errore
activity?.SetStatus(ActivityStatusCode.Error, exc.Message);
activity?.AddEvent(new ActivityEvent("exception", tags: new ActivityTagsCollection {
{ "exception.type", exc.GetType().Name },
{ "exception.message", exc.Message },
{ "exception.stacktrace", exc.StackTrace }
}));
}
}
}
else
{
LogTrace($"Server REDIS master non trovato", LogLevel.Error);
}
// aggiunta tags + log
activity?.SetTag("data.source", source);
activity?.SetTag("param.pat2Flush", pat2Flush);
LogTrace($"{pat2Flush} | {source} | trace: {activity?.TraceId} | {activity?.Duration.TotalMilliseconds}ms");
return answ;
}
/// <summary>
/// Helper trace messaggio log (SE abilitato)
/// </summary>
/// <param name="traceMsg"></param>
protected new void LogTrace(string traceMsg, NLog.LogLevel? reqLevel = null, [CallerMemberName] string? methodName = null)
{
if (!_traceEnabled)
return;
reqLevel ??= NLog.LogLevel.Debug;
// Loggo!
Log.Log(reqLevel, $"{methodName} | {traceMsg}");
}
#endregion Protected Methods
#region Private Fields
private new static Logger Log = LogManager.GetCurrentClassLogger();
private readonly IServiceProvider _serviceProvider;
private string redisBaseKey = "Lux:Cache";
#endregion Private Fields
#region Private Properties
private static DataSimulatorController dataSimController { get; set; } = null!;
private static LuxController dbController { get; set; } = null!;
#endregion Private Properties
}
}