using EgwCoreLib.Utils; using Microsoft.EntityFrameworkCore; using MP.Core.DTO; using MP.Core.Objects; using MP.Data; using MP.Data.Controllers; using MP.Data.DbModels; using MP.Data.MgModels; using MP.Data.Repository.Anag; using MP.Data.Services; using Newtonsoft.Json; using NLog; using StackExchange.Redis; using System.Data; using System.Diagnostics; using ZiggyCreatures.Caching.Fusion; namespace MP.SPEC.Data { public class MpDataService : IDisposable { #region Public Constructors private readonly IAnagRepository _anagRepository; public MpDataService(IConfiguration configuration, IFusionCache cache, IAnagRepository anagRepository) { // salvataggio oggetti _configuration = configuration; _cache = cache; _anagRepository = anagRepository; // Verifica conf trace... traceEnabled = _configuration.GetValue("Otel:EnableTracing", false); slowLogThresh = _configuration.GetValue("ServerConf:slowLogThresh", 1); Log.Info($"MpDataService | INIT | Trace enabled: {traceEnabled}"); // setup compoenti REDIS redisConn = ConnectionMultiplexer.Connect(_configuration.GetConnectionString("Redis") ?? "localhost:6379"); redisConnAdmin = ConnectionMultiplexer.Connect(_configuration.GetConnectionString("RedisAdmin") ?? "localhost:6379"); redisDb = redisConn.GetDatabase(); // leggo cache lungo/cordo periodo int.TryParse(_configuration.GetValue("ServerConf:redisShortTimeCache"), out redisShortTimeCache); int.TryParse(_configuration.GetValue("ServerConf:redisLongTimeCache"), out redisLongTimeCache); // setup MsgPipe BroadastMsgPipe = new MessagePipe(redisConn, Constants.BROADCAST_M_PIPE); Log.Info("MpDataService | Redis OK"); // conf DB string connStr = _configuration.GetConnectionString("MP.Data") ?? ""; if (string.IsNullOrEmpty(connStr)) { Log.Error("DbController: ConnString empty!"); } else { dbController = new MpSpecController(configuration); Log.Info("DbController OK"); } // conf x lettura dati da area REDIS di MP-IO MpIoNS = _configuration.GetValue("ServerConf:MpIoNS") ?? ""; // conf mongo... connStr = _configuration.GetConnectionString("mdbConnString") ?? ""; if (string.IsNullOrEmpty(connStr)) { Log.Error("MongoController: ConnString empty!"); } else { mongoController = new MpMongoController(configuration); Log.Info("MongoController OK"); } Log.Info("MpDataService | INIT completed"); } #endregion Public Constructors #region Public Events /// /// Evento richiesta rilettura dati pagina (x refresh pagine aperte) /// public event EventHandler ReloadRequest = delegate { }; #endregion Public Events #region Public Properties public MessagePipe BroadastMsgPipe { get; set; } = null!; /// /// Expiry DateTime x refresh pagina parametri /// public DateTime DtParamExpiry { get => _dtParamExpiry; set => _dtParamExpiry = value; } #endregion Public Properties #region Public Methods /// /// Recupera eventuali azioni richieste /// /// public async Task ActionGetReq() { using var activity = ActivitySource.StartActivity("ActionGetReq"); string source = "REDIS"; DisplayAction? result = null; // cerco in redis... RedisValue rawData = await redisDb.StringGetAsync(Utils.redisActionReq); if (!string.IsNullOrEmpty($"{rawData}")) { result = JsonConvert.DeserializeObject($"{rawData}"); } if (result == null) { result = new DisplayAction(); } activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"ActionGetReq | {source} | {activity?.Duration.TotalMilliseconds}ms"); return result; } /// /// Salva richiesta azione /// /// /// public async Task ActionSetReqAsync(DisplayAction? act2save) { using var activity = ActivitySource.StartActivity("ActionSetReqAsync"); string source = "REDIS"; bool fatto = false; // cerco in redis... string rawData = JsonConvert.SerializeObject(act2save); // invio broadcast + salvo in redis await BroadastMsgPipe.SaveAndSendMessageAsync(Utils.redisActionReq, rawData); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"ActionSetReqAsync {source} send to broadcast + Write cache: {activity?.Duration.TotalMilliseconds}ms"); return fatto; } /// /// Stacca un nuovo counter x il tipo richiesto /// /// public async Task AnagCountersGetNextAsync(string cntType) { using var activity = ActivitySource.StartActivity("AnagCountersGetNextAsync"); AnagCountersModel result = new AnagCountersModel(); string source = "DB"; result = await _anagRepository.AnagCountersGetNextAsync(cntType); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"AnagCountersGetNextAsync | {source} | {activity?.Duration.TotalMilliseconds}ms"); return result; } /// /// Elenco EVENTI validi x ogni macchina secondo conf standard macchina /// /// public async Task> AnagEventiGeneralAsync() { return await GetOrFetchAsync( operationName: "AnagEventiGeneralAsync", cacheKey: $"{Utils.redisEventList}:VSEB:GENERAL", expiration: GetRandTOut(redisLongTimeCache), fetchFunc: async () => { return await _anagRepository.AnagEventiGeneralAsync() ?? new List(); }, tagList: [Utils.redisEventList] ); } /// /// Delete record AnagraficaGruppi /// /// public async Task AnagGruppiDeleteAsync(AnagGruppiModel updRec) { using var activity = ActivitySource.StartActivity("AnagGruppiDeleteAsync"); bool result = false; string source = "DB"; result = await _anagRepository.AnagGruppiDeleteAsync(updRec); // elimino cache redis... await FlushFusionCacheAsync(Utils.redisAnagGruppi); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"AnagGruppiDeleteAsync | CodGruppo {updRec.CodGruppo} | {source}{activity?.Duration.TotalMilliseconds}ms"); return result; } /// /// Upsert record AnagraficaGruppi /// /// /// public async Task AnagGruppiUpsertAsync(AnagGruppiModel UpdRec) { using var activity = ActivitySource.StartActivity("AnagGruppiUpsertAsync"); bool result = false; string source = "DB"; result = await _anagRepository.AnagGruppiUpsertAsync(UpdRec); // elimino cache redis... await FlushFusionCacheAsync(Utils.redisAnagGruppi); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"AnagGruppiUpsertAsync | CodGruppo {UpdRec.CodGruppo} | {source} | {activity?.Duration.TotalMilliseconds}ms"); return result; } public async Task> AnagStatiCommAsync() { return await GetOrFetchAsync( operationName: "AnagStatiCommAsync", cacheKey: Utils.redisStatoCom, expiration: GetRandTOut(redisLongTimeCache), fetchFunc: async () => await dbController.AnagStatiCommAsync() ?? new List(), tagList: [Utils.redisStatoCom] ); } /// /// Restituisce elenco tipi articolo livello anagrafica /// /// public async Task> AnagTipoArtLvAsync() { return await GetOrFetchAsync( operationName: "AnagTipoArtLvAsync", cacheKey: Utils.redisTipoArt, expiration: GetRandTOut(redisLongTimeCache), fetchFunc: async () => await dbController.AnagTipoArtLvAsync() ?? new List(), tagList: [Utils.redisTipoArt] ); } /// /// Elenco Codice articolo con dati dossier gestiti /// /// public async Task> ArticleWithDossierAsync() { return await GetOrFetchAsync( operationName: "ArticleWithDossierAsync", cacheKey: Utils.redisArtByDossier, expiration: GetRandTOut(redisLongTimeCache), fetchFunc: async () => await dbController.ArticleWithDossierAsync() ?? new List(), tagList: [Utils.redisArtByDossier] ); } /// /// Conteggio articoli data ricerca /// /// /// /// /// public async Task ArticoliCountSearchAsync(string tipo = "*", string azienda = "*", string searchVal = "") { string sKey = string.IsNullOrWhiteSpace(tipo) ? "ALL" : tipo.Trim(); string redisKey = $"{Utils.redisArtList}:{azienda}:{sKey}:{searchVal}:Count"; return await GetOrFetchAsync( operationName: "ArticoliCountSearchAsync", cacheKey: redisKey, expiration: GetRandTOut(redisLongTimeCache), fetchFunc: async () => await dbController.ArticoliCountSearchAsync(tipo, azienda, searchVal), tagList: [Utils.redisArtList, $"{Utils.redisArtList}:CountSearch"] ); } /// /// Eliminazione record selezionato /// /// /// public async Task ArticoliDeleteRecord(AnagArticoliModel currRec) { using var activity = ActivitySource.StartActivity("ArticoliDeleteRecordAsync"); string source = "DB"; bool fatto = await dbController.ArticoliDeleteRecordAsync(currRec); await FlushFusionCacheArticoli(); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"ArticoliDeleteRecordAsync | {source} | {activity?.Duration.TotalMilliseconds}ms"); return fatto; } /// /// Restitusice elenco articoli dato tipo (es KIT) /// /// /// /// public async Task> ArticoliGetByTipoAsync(string tipo, string azienda = "*") { string sKey = string.IsNullOrWhiteSpace(tipo) ? "ALL" : tipo.Trim(); string redisKey = $"{Utils.redisArtList}:{azienda}:{sKey}"; return await GetOrFetchAsync( operationName: "ArticoliGetByTipoAsync", cacheKey: redisKey, expiration: GetRandTOut(redisLongTimeCache), fetchFunc: async () => await dbController.ArticoliGetByTipoAsync(tipo, azienda) ?? new List(), tagList: [Utils.redisArtList, $"{Utils.redisArtList}:Tipo"] ); } /// /// Restitusice elenco articoli cercati /// /// /// /// public async Task> ArticoliGetSearchAsync(int numRecord, string tipoArt, string azienda, string searchVal) { string sKey = string.IsNullOrWhiteSpace(searchVal) ? "***" : searchVal.Trim(); string redisKey = $"{Utils.redisArtList}:{tipoArt}:{azienda}:{sKey}:{numRecord}"; return await GetOrFetchAsync( operationName: "ArticoliGetSearchAsync", cacheKey: redisKey, expiration: GetRandTOut(redisLongTimeCache), fetchFunc: async () => await _anagRepository.ArticoliGetSearchAsync(numRecord, tipoArt, azienda, searchVal) ?? new List(), //await dbController.ArticoliGetSearchAsync(numRecord, tipoArt, azienda, searchVal) ?? new List(), tagList: [Utils.redisArtList, $"{Utils.redisArtList}:Search"] ); } /// /// Elenco articoli contenuti in Kit (come child), non eliminabli /// /// public async Task> ArticoliInKitAsync() { string redisKey = $"{Utils.redisArtList}:InKit"; return await GetOrFetchAsync( operationName: "ArticoliInKitAsync", cacheKey: redisKey, expiration: GetRandTOut(redisLongTimeCache), fetchFunc: async () => await dbController.ArticoliInKitAsync() ?? new List(), tagList: [Utils.redisArtList, $"{Utils.redisArtList}:InKit"] ); } /// /// Aggiornamento record selezionato /// /// /// public async Task ArticoliUpdateRecord(AnagArticoliModel currRec) { using var activity = ActivitySource.StartActivity("ArticoliUpdateRecord"); string source = "DB"; bool fatto = await dbController.ArticoliUpdateRecord(currRec); await FlushFusionCacheArticoli(); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"ArticoliUpdateRecord | {source} | {activity?.Duration.TotalMilliseconds}ms"); return fatto; } /// /// Verifica se sia possiubile cancellare articolo dato suo CodArt cercando su redis o su /// tab veto da DB /// /// /// public bool ArticoloDelEnabled(object CodArt) { using var activity = ActivitySource.StartActivity("ArticoloDelEnabled"); string codArticolo = $"{CodArt}"; int numUsed = _listCodArtUsed.Count; int numUnused = _listCodArtNotUsed.Count; bool usato = true; string source = "MEMORY"; // 1. Controllo immediato sulla cache locale (HashSet) x eventuale refresh if (DateTime.Now >= _artCacheExpiry || (numUsed + numUnused) <= 0) { source = "DB/REDIS"; // Fallback sincrono minimo per non rompere il componente Blazor // Nota: Questo è un workaround per la firma sincrona. var task = EnsureArtCacheLoadedAsync(false); task.Wait(); // rileggo numUsed = _listCodArtUsed.Count; numUnused = _listCodArtNotUsed.Count; } // verifico quale sia l'elenco if (numUsed > 0) { usato = _listCodArtUsed.Contains(codArticolo); } else { usato = !_listCodArtNotUsed.Contains(codArticolo); } // verifico infine anche che NON sia nell'elenco degli articoli in KIT if (!usato) { usato = _listCodArtInKit.Contains(CodArt); } activity?.SetTag("data.source", source); activity?.Stop(); if (activity?.Duration.TotalMilliseconds > slowLogThresh) { LogTrace($"ArticoloDelEnabled | Cod: {codArticolo} | {source} | {activity?.Duration.TotalMilliseconds}ms"); } return !usato; } public string CalcRecipe(RecipeModel currRecipe) { using var activity = ActivitySource.StartActivity("CalcRecipe"); var result = mongoController.CalcRecipe(currRecipe); activity?.SetTag("data.source", "MONGO"); return result; } /// /// Recupero tab config in modalità Asincrona /// /// public async Task> ConfigGetAllAsync() { return await GetOrFetchAsync( operationName: "ConfigGetAllAsync", cacheKey: Utils.redisConfAll, expiration: GetRandTOut(redisLongTimeCache * 2), fetchFunc: async () => await dbController.ConfigGetAllAsync() ?? new List(), tagList: [Utils.redisConfAll] ); } /// /// Reset dati cache config /// /// public async Task ConfigResetCacheAsync() { using var activity = ActivitySource.StartActivity("ConfigResetCacheAsync"); string source = "REDIS"; await FlushFusionCacheConfig(); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"ConfigResetCacheAsync | {source} | {activity?.Duration.TotalMilliseconds}ms"); } /// /// Restituisce valore della stringa (SE disponibile) - modalità async /// /// /// public async Task ConfigTryGetAsync(string keyName) { using var activity = ActivitySource.StartActivity("ConfigTryGetAsync"); string source = "MEMORY"; await EnsureConfigLoadedAsync(); _configData.TryGetValue(keyName, out var value); activity?.SetTag("data.source", source); activity?.Stop(); if (activity?.Duration.TotalMilliseconds > slowLogThresh) { LogTrace($"ConfigTryGetAsync | {keyName} | {source} | {activity?.Duration.TotalMilliseconds}ms"); } return value ?? ""; } /// /// Update chiave config /// /// public async Task ConfigUpdateAsync(ConfigModel updRec) { using var activity = ActivitySource.StartActivity("ConfigUpdateAsync"); string source = "DB"; var updRes = await dbController.ConfigUpdateAsync(updRec); await FlushFusionCacheConfig(); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"ConfigUpdateAsync Read from {source}: {activity?.Duration.TotalMilliseconds}ms"); return updRes; } /// /// Restituisce le statistiche di DB maintenance eseguite /// /// public async Task> DbDedupStatsAsync() { using var activity = ActivitySource.StartActivity("DbDedupStatsAsync"); string source = "REDIS"; Dictionary actStats = new Dictionary(); string currKey = $"{Utils.redisStatsDbMaint}"; // recupero i record statistiche correnti RedisValue rawData = redisDb.StringGet(currKey); if (rawData.HasValue) { var rawStats = JsonConvert.DeserializeObject>($"{rawData}"); if (rawStats != null) { actStats = rawStats; } } activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"DbDedupStatsAsync Read from {source}: {activity?.Duration.TotalMilliseconds}ms"); return actStats; } /// /// Dispose del connettore ai dati /// public void Dispose() { // Clear database controller mongoController.Dispose(); redisConn.Dispose(); } /// /// Eliminazione di un dossier /// /// record dossier da eliminare /// public async Task DossiersDeleteRecordAsync(DossierModel selRecord) { using var activity = ActivitySource.StartActivity("DossiersDeleteRecordAsync"); bool result = false; result = await dbController.DossiersDeleteRecordAsync(selRecord); // elimino cache... await FlushFusionCacheAsync(Utils.redisDossByMac); activity?.SetTag("data.source", "DB"); activity?.Stop(); LogTrace($"DossiersDeleteRecordAsync | IdxMacchina {selRecord.IdxMacchina} | DtRif {selRecord.DtRif} | IdxODL {selRecord.IdxODL} | {activity?.Duration.TotalMilliseconds}ms"); return result; } /// /// Elenco ultimi n record DOssiers (che contengono ad esempio "salvataggi" di FLuxLog) dato /// idxMaccSel (ordinato x data registrazione) /// /// * = tutte, altrimenti solo x una data idxMaccSel /// Data minima per estrazione records /// Data Massima per estrazione records /// Num Max records da recuperare /// public async Task> DossiersGetLastFiltAsync(string IdxMacchina, string CodArticolo, DateTime DtStart, DateTime DtEnd, int MaxRec) { string currKey = $"{Utils.redisDossByMac}:{IdxMacchina}:{CodArticolo}:{DtStart:yyyyMMddHHmm}:{DtEnd:yyyyMMddHHmm}:{MaxRec}"; return await GetOrFetchAsync( operationName: "DossiersGetLastFiltAsync", cacheKey: currKey, expiration: GetRandTOut(redisLongTimeCache * 5), fetchFunc: async () => await dbController.DossiersGetLastFiltAsync(IdxMacchina, CodArticolo, DtStart, DtEnd, MaxRec) ?? new List(), tagList: [Utils.redisDossByMac] ); } /// /// Inserimento nuovo record dossier /// /// /// public async Task DossiersInsert(DossierModel currDoss) { using var activity = ActivitySource.StartActivity("DossiersInsertAsync"); string source = "DB"; // aggiorno record sul DB bool answ = await dbController.DossiersInsertAsync(currDoss); answ = await FlushFusionCacheAsync(Utils.redisDossByMac); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"DossiersInsertAsync | {source} | {activity?.Duration.TotalMilliseconds}ms"); return answ; } /// /// Effettua salvataggio snapshot parametri (con stored) + svuota eventuale cache redis /// /// idxMaccSel /// NUm massimo secondi per recuperare dati correnti /// DataOra riferimento x cui prendere valori antecedenti /// public async Task DossiersTakeParamsSnapshotLast(string IdxMacchina, DateTime dtMin, DateTime dtMax) { using var activity = ActivitySource.StartActivity("DossiersUpdateValoreAsync"); string source = "DB"; bool answ = false; Log.Info($"Richiesta snapshot per idxMaccSel {IdxMacchina} | periodo {dtMin} --> {dtMax}"); // chiamo stored x salvare parametri await dbController.DossiersTakeParamsSnapshotLastAsync(IdxMacchina, dtMin, dtMax); // elimino cache... answ = await FlushFusionCacheAsync(Utils.redisDossByMac); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"DossiersTakeParamsSnapshotLastAsync | Svuotata cache dossier | {source} | {activity?.Duration.TotalMilliseconds}ms"); return answ; } /// /// Update valore dossier /// /// /// public async Task DossiersUpdateValoreAsync(DossierModel currDoss) { using var activity = ActivitySource.StartActivity("DossiersUpdateValoreAsync"); string source = "DB"; // aggiorno record sul DB bool answ = await dbController.DossiersUpdateValoreAsync(currDoss); answ = await FlushFusionCacheAsync(Utils.redisDossByMac); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"DossiersUpdateValoreAsync | {source} | {activity?.Duration.TotalMilliseconds}ms"); return answ; } /// /// Restituisce elenco aziende /// /// public async Task> ElencoAziendeAsync() { return await GetOrFetchAsync( operationName: "ElencoAziendeAsync", cacheKey: $"{Utils.redisAnagGruppi}:Aziende", expiration: GetRandTOut(redisLongTimeCache * 2), fetchFunc: async () => await _anagRepository.AnagGruppiAziendeAsync() ?? new List(), tagList: [Utils.redisAnagGruppi, $"{Utils.redisAnagGruppi}:Aziende"] ); } /// /// Restituisce elenco Fasi /// /// public async Task> ElencoGruppiFaseAsync() { return await GetOrFetchAsync( operationName: "ElencoGruppiFaseAsync", cacheKey: $"{Utils.redisAnagGruppi}:FASE", expiration: GetRandTOut(redisLongTimeCache), fetchFunc: async () => await _anagRepository.AnagGruppiFaseAsync() ?? new List(), tagList: [Utils.redisAnagGruppi] ); } /// /// Elenco link validi per il menu /// /// public async Task> ElencoLinkAsync() { return await GetOrFetchAsync( operationName: "ElencoLinkAsync", cacheKey: Utils.redisLinkMenu, expiration: GetRandTOut(redisLongTimeCache), fetchFunc: async () => await dbController.ElencoLinkAsync() ?? new List(), tagList: [Utils.redisLinkMenu] ); } /// /// Restitusice elenco Reparti /// /// public async Task> ElencoRepartiDtoAsync() { return await GetOrFetchAsync( operationName: "ElencoRepartiDtoAsync", cacheKey: $"{Utils.redisAnagGruppi}:REPARTO", expiration: GetRandTOut(redisLongTimeCache), fetchFunc: async () => await _anagRepository.AnagGruppiRepartoDtoAsync() ?? new(), tagList: [Utils.redisAnagGruppi] ); } /// /// Caricamento asincrono della cache degli articoli (Used/Unused) /// public async Task EnsureArtCacheLoadedAsync(bool forceReload) { if (!forceReload && (DateTime.Now < _artCacheExpiry && (_listCodArtUsed.Count > 0 || _listCodArtNotUsed.Count > 0))) return; try { // verifico quale sia il set + piccolo int totalCount = await dbController.ArticoliCountAsync(); int usedCount = await dbController.ArticoliCountUsedAsync(); if (usedCount <= (totalCount - usedCount)) { var usedList = await dbController.ArticoliGetUsedAsync(); _listCodArtUsed = new HashSet(usedList.Select(x => x.CodArticolo)); _listCodArtNotUsed.Clear(); } else { var unusedList = await dbController.ArticoliGetUnusedAsync(); _listCodArtNotUsed = new HashSet(unusedList.Select(x => x.CodArticolo)); _listCodArtUsed.Clear(); } // calcolo anche elenco articoli impiegati in istanzanKIT var listInKit = await dbController.ArticoliInKitAsync(); _listCodArtInKit = new HashSet(listInKit.Select(x => x.CodArticolo)); _artCacheExpiry = DateTime.Now.AddMinutes(15); // TTL ragionevole per la cache locale } catch (Exception ex) { Log.Error($"Errore nel caricamento cache articoli: {ex.Message}"); _artCacheExpiry = DateTime.Now.AddSeconds(1); // Retry breve in caso di errore } } /// /// Aggiunta record EventList /// /// /// public async Task EvListInsert(EventListModel newRec) { using var activity = ActivitySource.StartActivity("EvListInsertAsync"); string source = "DB"; var result = await dbController.EvListInsertAsync(newRec); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"EvListInsertAsync | Read from {source}: {activity?.Duration.TotalMilliseconds}ms"); return result; } /// /// Flush cache relativa a MP-IO x dati ODL /// /// public async Task FlushRedisCacheMpIoOdl() { using var activity = ActivitySource.StartActivity("FlushRedisCacheMpIoOdl"); string source = "REDIS"; // svuoto dalla cache REDIS del server IO... bool ok01 = await ResetIoCache("CurrODL"); bool ok02 = await ResetIoCache("CurrOdlRow"); bool ok03 = await ResetIoCache("CurrStatoMacc"); bool ok04 = await ResetIoCache("DtMac"); activity?.SetTag("data.source", "REDIS"); activity?.Stop(); LogTrace($"FlushRedisCacheMpIoOdl | Read from {source}: {activity?.Duration.TotalMilliseconds}ms"); return ok01 && ok02 && ok03 && ok04; } /// /// Funzione di Data Reduction x FluxLog /// /// Macchina /// Elenco FL da processare /// Periodo /// modalità sel valore /// intervallo di analisi /// max num per intervallo /// public async Task FluxLogDataRedux(string idxMaccSel, List fluxList, DtUtils.Periodo currPeriodo, Enums.ValSelection valMode, Enums.DataInterval intReq, int maxItem) { using var activity = ActivitySource.StartActivity("FluxLogDataReduxAsync"); string source = "DB"; List procStats = await dbController.FluxLogDataReduxAsync(idxMaccSel, fluxList, currPeriodo, valMode, intReq, maxItem); // effettuo merge statistiche... await ProcDedupStatMergeAsync(procStats); // svuoto cache await FlushFusionCacheFluxLog(); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"FluxLogDataReduxAsync | Read from {source}: {activity?.Duration.TotalMilliseconds}ms"); } /// /// Helper conversione valore raw in List di FluxLogDTO /// /// /// public List FluxLogDtoConvert(string Valore) { List answ = new List(); DossierFluxLogDTO? result = JsonConvert.DeserializeObject(Valore); if (result != null) { if (result.ODL != null) { answ = result .ODL .OrderBy(x => x.CodFlux) .ToList(); // inizializzo SE necessario foreach (var item in answ) { item.ValoreEdit = String.IsNullOrEmpty(item.ValoreEdit) ? item.Valore : item.ValoreEdit; } } } return answ; } /// /// Elenco FluxLog in modalità filtro /// /// Data massima x eventi /// Data minima x eventi /// * = tutte, altrimenti solo x una data idxMaccSel /// *=tutti, altrimenti solo selezionato /// numero massimo record da restituire /// durata cache in secondi /// public async Task> FluxLogGetLastFiltAsync(DateTime DtMax, DateTime DtMin, string IdxMacchina, string CodFlux, int MaxRec, double redisCacheSec) { string currKey = $"{Utils.redisFluxLogFilt}:{IdxMacchina}:{CodFlux}:{MaxRec}:{DtMax:yyyyMMddHHmm}:{DtMin:yyyyMMddHHmm}"; return await GetOrFetchAsync( operationName: "FluxLogGetLastFiltAsync", cacheKey: currKey, expiration: TimeSpan.FromSeconds(redisCacheSec), fetchFunc: async () => await dbController.FluxLogGetLastFiltAsync(DtMax, DtMin, IdxMacchina, CodFlux, MaxRec) ?? new List(), tagList: [Utils.redisFluxLogFilt] ); } /// /// Elenco FluxLog in modalità Pareto /// /// public async Task> FluxLogParetoAsync(string idxMacchina, DateTime dtFrom, DateTime dtTo) { string redKey = $"{Utils.redisParetoFLKey}:{idxMacchina}:{dtFrom:yyyyMMdd}:{dtTo:yyyyMMdd}"; return await GetOrFetchAsync( operationName: "FluxLogParetoAsync", cacheKey: redKey, expiration: GetRandTOut(redisLongTimeCache), fetchFunc: async () => await dbController.FluxLogParetoAsync(idxMacchina, dtFrom, dtTo) ?? new List(), tagList: [Utils.redisParetoFLKey] ); } /// /// Stored manutenzione del DB /// /// Esegue realmente il task /// Aggiornamento statistiche /// Salvataggio /// def: 1000 /// def: 10 /// def: 50 /// public async Task ForceDbMaintAsync(bool doExec = true, bool doUpdStat = true, bool doSave = true, int minPgCnt = 1000, int minAvgFrag = 10, int maxAvgFragReb = 50) { using var activity = ActivitySource.StartActivity("ForceDbMaintAsync"); string source = "DB"; await dbController.ForceDbMaintAsync(doExec, doUpdStat, doSave, minPgCnt, minAvgFrag, maxAvgFragReb); // svuoto cache await FlushFusionCacheFluxLog(); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"ForceDbMaintAsync | {source} | {activity?.Duration.TotalMilliseconds}ms"); // registro statistiche esecuzione await RecDbMaintStatAsync(activity?.Duration ?? TimeSpan.FromSeconds(1)); } /// /// Cancellazione FusionCache (totale) - wrapper public /// /// public Task ForceFlushFusionCacheAsync() { return FlushFusionCacheAsync(); } /// /// Cancellazione RedisCache forzata /// /// public async Task ForceFlushRedisCache() { using var activity = ActivitySource.StartActivity("ForceFlushRedisCache"); string source = "FUSION"; // valutare se tenere RedisValue pattern = Utils.RedValue("*"); await ExecFlushRedisPatternAsync(pattern); // pulisco fusionlog cache... bool answ = await FlushFusionCacheAsync(); activity?.Stop(); LogTrace($"FlushCache | {source} | {activity?.Duration.TotalMilliseconds}ms"); return answ; } /// /// Eliminazione di un record macchina dal gruppo /// /// /// public async Task Grp2MaccDeleteAsync(Gruppi2MaccModel rec2del) { using var activity = ActivitySource.StartActivity("Grp2MaccDeleteAsync"); bool result = false; result = await dbController.Grp2MaccDeleteAsync(rec2del); // elimino cache redis... await FlushFusionCacheMacGrp(); activity?.SetTag("data.source", "DB"); activity?.Stop(); LogTrace($"Grp2MaccDeleteAsync | CodGruppo {rec2del.CodGruppo} | IdxMacc {rec2del.IdxMacchina} | {activity?.Duration.TotalMilliseconds}ms"); return result; } /// /// Insert di un record macchina /// /// /// public async Task Grp2MaccInsertAsync(Gruppi2MaccModel upsRec) { using var activity = ActivitySource.StartActivity("Grp2MaccInsertAsync"); bool result = false; result = await dbController.Grp2MaccInsertAsync(upsRec); // elimino cache redis... await FlushFusionCacheMacGrp(); activity?.SetTag("data.source", "DB"); activity?.Stop(); LogTrace($"Grp2MaccInsertAsync | CodGruppo {upsRec.CodGruppo} | IdxMacc {upsRec.IdxMacchina} | {activity?.Duration.TotalMilliseconds}ms"); return result; } /// /// Eliminazione di un record operatore dal gruppo /// /// /// public async Task Grp2OperDeleteAsync(Gruppi2OperModel rec2del) { using var activity = ActivitySource.StartActivity("Grp2OperDeleteAsync"); bool result = false; result = await dbController.Grp2OperDeleteAsync(rec2del); // elimino cache redis... await FlushFusionCacheOprGrp(); activity?.SetTag("data.source", "DB"); activity?.Stop(); LogTrace($"Grp2OperDeleteAsync | CodGruppo {rec2del.CodGruppo} | MatrOpr {rec2del.MatrOpr} | {activity?.Duration.TotalMilliseconds}ms"); return result; } /// /// Insert di un record operatore /// /// /// public async Task Grp2OperInsertAsync(Gruppi2OperModel upsRec) { using var activity = ActivitySource.StartActivity("Grp2OperInsertAsync"); bool result = false; result = await dbController.Grp2OperInsertAsync(upsRec); // elimino cache redis... await FlushFusionCacheOprGrp(); activity?.SetTag("data.source", "DB"); activity?.Stop(); LogTrace($"Grp2OperInsertAsync | CodGruppo {upsRec.CodGruppo} | MatrOpr {upsRec.MatrOpr} | {activity?.Duration.TotalMilliseconds}ms"); return result; } /// /// Init ricetta /// /// /// /// /// public RecipeModel InitRecipe(string confPath, int idxPODL, Dictionary CalcArgs) { return mongoController.InitRecipe(confPath, idxPODL, CalcArgs); } /// /// Elimina record + svuotamento cache /// /// public async Task IstKitDeleteAsync(IstanzeKitModel currRecord) { using var activity = ActivitySource.StartActivity("IstKitDeleteAsync"); string source = "DB"; // salvo bool fatto = await dbController.IstKitDeleteAsync(currRecord); // svuoto cache await FlushFusionCacheKit(); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"IstKitDeleteAsync | Read from {source}: {activity?.Duration.TotalMilliseconds}ms"); return fatto; } /// /// Elenco Istanze KIT da ricerca /// /// /// /// public async Task> IstKitFiltAsync(string keyKit, string keyExtOrd) { string currKey = $"{Utils.redisKitInst}:{keyKit}:{keyExtOrd}"; return await GetOrFetchAsync( operationName: "IstKitFiltAsync", cacheKey: currKey, expiration: GetRandTOut(redisLongTimeCache), fetchFunc: async () => await dbController.IstKitFiltAsync(keyKit, keyExtOrd) ?? new List(), tagList: [Utils.redisKitInst] ); } /// /// Effettua creazione istanza KIT /// /// Articolo KIT (fittizio) /// Chiave x filtro conf su tab WKS public async Task IstKitInsertByWKSAsync(string CodArtParent, string KeyFilt) { using var activity = ActivitySource.StartActivity("IstKitInsertByWKSAsync"); string source = "DB"; // salvo bool fatto = await dbController.IstKitInsertByWKSAsync(CodArtParent, KeyFilt); // svuoto cache await FlushFusionCacheKit(); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"IstKitInsertByWKSAsync | {source} | {activity?.Duration.TotalMilliseconds}ms"); return fatto; } /// /// Esegue salvataggio record + svuotamento cache /// /// public async Task IstKitUpsertAsync(IstanzeKitModel currRecord) { using var activity = ActivitySource.StartActivity("IstKitUpsertAsync"); string source = "DB"; // salvo bool fatto = await dbController.IstKitUpsertAsync(currRecord); // svuoto cache await FlushFusionCacheKit(); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"IstKitUpsertAsync | {source} | {activity?.Duration.TotalMilliseconds}ms"); return fatto; } /// /// Elenco giacenze filtrate x IdxOdl /// /// id odl da cercare /// public async Task> ListGiacenzeAsync(int IdxOdl) { string currKey = $"{Utils.redisGiacenzaList}:{IdxOdl}"; return await GetOrFetchAsync( operationName: "ListGiacenzeAsync", cacheKey: currKey, expiration: GetRandTOut(redisShortTimeCache), fetchFunc: async () => await dbController.ListGiacenzeAsync(IdxOdl) ?? new List(), tagList: [Utils.redisGiacenzaList] ); } /// /// Recupero elenco PODL filtrati /// /// /// True = aperti (=senza ODL) /// public async Task> ListPODL_ByCodArtAsync(string CodArticolo, bool OnlyAvail) { string avType = OnlyAvail ? "Avail" : "ALL"; string currKey = $"{Utils.redisPOdlByCodArt}:{CodArticolo}:{avType}"; return await GetOrFetchAsync( operationName: "ListPODL_ByCodArtAsync", cacheKey: currKey, expiration: GetRandTOut(redisLongTimeCache), fetchFunc: async () => await dbController.ListPODL_ByCodArtAsync(CodArticolo, OnlyAvail) ?? new(), tagList: [Utils.redisPOdlByCodArt] ); } /// /// Elenco di tutte le macchine filtrate x gruppo /// /// /// public async Task> MacchineGetFiltAsync(string codGruppo) { string keyGrp = codGruppo != "*" ? codGruppo : "ALL"; string redisKey = $"{Utils.redisMacList}:{keyGrp}"; return await GetOrFetchAsync( operationName: "MacchineGetFiltAsync", cacheKey: redisKey, expiration: GetRandTOut(redisLongTimeCache), fetchFunc: async () => await dbController.MacchineGetFiltAsync(codGruppo) ?? new List(), tagList: [Utils.redisMacList] ); } /// /// Verifica se la idxMaccSel abbia un codice PATH ricette associato /// /// /// public async Task MacchineRecipeArchiveAsync(string idxMacchina) { string currKey = $"{Utils.redisMacRecipePath}:{idxMacchina}"; return await GetOrFetchAsync( operationName: "MacchineRecipeArchiveAsync", cacheKey: currKey, expiration: GetRandTOut(redisLongTimeCache), fetchFunc: async () => { var machineList = await MacchineGetFiltAsync("*"); var currMach = machineList.FirstOrDefault(x => x.IdxMacchina == idxMacchina); return currMach?.RecipeArchivePath ?? ""; }, tagList: [Utils.redisMacRecipePath] ); } /// /// Verifica se la idxMaccSel abbia un codice CONF ricetta associato /// /// /// public async Task MacchineRecipeConfAsync(string idxMacchina) { string currKey = $"{Utils.redisMacRecipeConf}:{idxMacchina}"; return await GetOrFetchAsync( operationName: "MacchineRecipeConfAsync", cacheKey: currKey, expiration: GetRandTOut(redisLongTimeCache), fetchFunc: async () => { var machineList = await MacchineGetFiltAsync("*"); var currMach = machineList.FirstOrDefault(x => x.IdxMacchina == idxMacchina); return currMach?.RecipePath ?? ""; }, tagList: [Utils.redisMacRecipeConf] ); } /// /// Elenco id Macchine che abbiano dati FLuxLog, nel periodo indicato /// /// /// /// public async Task> MacchineWithFluxAsync(DateTime dtStart, DateTime dtEnd) { string currKey = $"{Utils.redisMacByFlux}:{dtStart:yyyyMMddHHmm}:{dtEnd:yyyyMMddHHmm}"; return await GetOrFetchAsync( operationName: "MacchineWithFluxAsync", cacheKey: currKey, expiration: GetRandTOut(redisLongTimeCache), fetchFunc: async () => await dbController.MacchineWithFluxAsync(dtStart, dtEnd) ?? new List(), tagList: [Utils.redisMacByFlux] ); } public async Task> MachineWithOdlAsync() { string redisKey = Utils.redisOdlCurrByMac; return await GetOrFetchAsync( operationName: "MachineWithOdlAsync", cacheKey: redisKey, expiration: GetRandTOut(redisShortTimeCache), fetchFunc: async () => { var rawData = await dbController.OdlGetCurrentAsync(); var dbResult = rawData .Select(x => x.IdxMacchina) .Distinct() .ToList(); return dbResult ?? new List(); }, tagList: [Utils.redisOdlCurrByMac] ); } /// /// Recupero info Machine-IOB x TAB (da info registrate IOB-WIN --> MP-IO) /// /// /// public async Task> MachIobConfAsync(string IdxMacchina) { string redisKey = Utils.redisIobConf; return await GetOrFetchAsync( operationName: "MachIobConfAsync", cacheKey: redisKey, expiration: GetRandTOut(redisShortTimeCache), fetchFunc: async () => { Dictionary result = new Dictionary(); // cerco in redis... string currKey = RedHashMpIO($"IOB:{IdxMacchina}:MachIobConfAsync"); if (await redisDb.KeyExistsAsync(currKey)) { result = (await redisDb.HashGetAllAsync(currKey)) .ToDictionary(x => $"{x.Name}", x => $"{x.Value}"); } return result; }, tagList: [Utils.redisIobConf] ); } /// /// Elenco MSE stato amcchine /// /// Force refresh from DB /// public async Task> MseGetAllAsync(bool forceDb = false) { using var activity = ActivitySource.StartActivity("MseGetAllAsync"); if (forceDb) { await _cache.RemoveAsync(Constants.redisMseKey); } return await GetOrFetchAsync( operationName: "MseGetAllAsync", cacheKey: Constants.redisMseKey, expiration: TimeSpan.FromSeconds(1), fetchFunc: async () => await dbController.MseGetAllAsync(2000) ?? new(), tagList: [Constants.redisMseKey] ); } /// /// Invio notifica rilettura (con parametro) /// /// public void NotifyReloadRequest(string message) { if (ReloadRequest != null) { // messaggio ReloadEventArgs rea = new ReloadEventArgs(message); ReloadRequest.Invoke(this, rea); } } /// /// Elenco ODL dato batch selezionato /// /// Batch richiesto /// public async Task> OdlByBatchAsync(string BatchSel) { return await GetOrFetchAsync( operationName: "OdlByBatchAsync", cacheKey: Utils.redisOdlByBatch, expiration: GetRandTOut(redisLongTimeCache), fetchFunc: async () => await dbController.OdlByBatchAsync(BatchSel), tagList: [Utils.redisOdlByBatch] ); } /// /// ODL da chiave /// /// /// public async Task OdlByKeyAsync(int IdxOdl) { string currKey = $"{Utils.redisOdlByKey}:{IdxOdl}"; return await GetOrFetchAsync( operationName: "OdlByKeyAsync", cacheKey: currKey, expiration: GetRandTOut(redisLongTimeCache), fetchFunc: async () => await dbController.OdlByKeyAsync(IdxOdl), tagList: [Utils.redisOdlByKey] ); } /// /// Effettua chiusura dell'ODL indicato, andand /// /// idx odl da chiudere /// idx idxMaccSel /// matricola operatore /// indica se confermare i pezzi priam di chiudere ODL public async Task ODLCloseAsync(int idxOdl, string idxMacchina, int matrOpr, bool confPezzi) { using var activity = ActivitySource.StartActivity("ODLCloseAsync"); string source = "DB"; bool fatto = false; await EnsureConfigLoadedAsync(); bool confRett = false; _configData.TryGetValue("confRett", out var value); if (!string.IsNullOrEmpty(value)) { bool.TryParse(value, out confRett); } int modoConfProd = 0; _configData.TryGetValue("modoConfProd", out var vModo); if (!string.IsNullOrEmpty(value)) { int.TryParse(vModo, out modoConfProd); } // chiamo metodo conferma! fatto = await dbController.ODLCloseAsync(idxOdl, idxMacchina, matrOpr, confPezzi, confRett, modoConfProd); await FlushFusionCacheAsync(Utils.redisOdlByKey); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"ODLCloseAsync | Read from {source}: {activity?.Duration.TotalMilliseconds}ms"); return fatto; } /// /// Elenco ODL filtrati x stato, articolo, KeyRich (che contiene stato) /// /// Stato ODL: true=in corso/completato /// Cod articolo /// KeyRich (parziale) da cercare (es cod stato x yacht) /// Reparto selezionato /// Macchina selezionata /// Data inizio /// Data fine /// public async Task> OdlListGetFiltAsync(bool inCorso, string codArt, string keyRichPart, string Reparto, string IdxMacchina, DateTime startDate, DateTime endDate) { string currKey = $"{Utils.redisOdlList}:{inCorso}:{codArt}:{keyRichPart}:{Reparto}:{IdxMacchina}:{startDate:yyyyMMdd_HHmmss}:{endDate:yyyyMMdd_HHmmss}"; return await GetOrFetchAsync( operationName: "OdlListGetFiltAsync", cacheKey: currKey, expiration: GetRandTOut(redisShortTimeCache), fetchFunc: async () => await dbController.ListODLFiltAsync(inCorso, codArt, keyRichPart, Reparto, IdxMacchina, startDate, endDate) ?? new(), tagList: [Utils.redisOdlList] ); } /// /// Statistiche ODL calcolate (da stored stp_STAT_ODL) /// /// public async Task> OdlStatsAsync(int IdxOdl) { string currKey = $"{Utils.redisOdlStats}:{IdxOdl}"; return await GetOrFetchAsync( operationName: "OdlStatsAsync", cacheKey: currKey, expiration: GetRandTOut(redisShortTimeCache), fetchFunc: async () => await dbController.OdlGetStatAsync(IdxOdl) ?? new(), tagList: [Utils.redisOdlStats] ); } /// /// Elenco operatori filtrati x gruppo /// /// /// public async Task> OperatoriGetFiltAsync(string codGruppo) { string keyGrp = codGruppo != "*" ? codGruppo : "ALL"; string currKey = $"{Utils.redisOprList}:{keyGrp}"; return await GetOrFetchAsync( operationName: "OperatoriGetFiltAsync", cacheKey: currKey, expiration: GetRandTOut(redisLongTimeCache), fetchFunc: async () => await dbController.OperatoriGetFiltAsync(codGruppo) ?? new List(), tagList: [Utils.redisOprList] ); } /// /// Elenco di tutti i parametri filtrati x idxMaccSel /// /// * = tutte, altrimenti solo x una data idxMaccSel /// public async Task> ParametriGetFiltAsync(string IdxMacchina) { string currKey = $"{Utils.redisFluxByMac}:{IdxMacchina}"; return await GetOrFetchAsync( operationName: "ParametriGetFiltAsync", cacheKey: currKey, expiration: GetRandTOut(redisShortTimeCache), fetchFunc: async () => await dbController.ParametriGetFiltAsync(IdxMacchina) ?? new(), tagList: [Utils.redisFluxByMac] ); } /// /// Restituisce dizionario ODL/PODL data lista IdxOdl /// /// /// public async Task> PODL_getDictOdlPodlAsync(List idxOdlList) { if (idxOdlList == null || !idxOdlList.Any()) return new Dictionary(); var distinctIds = idxOdlList.Distinct().ToList(); var resultDictionary = new Dictionary(); var missingIds = new List(); // STEP 1: Controllo rapido in FusionCache (L1/Memory cache) foreach (var id in distinctIds) { var cacheKey = $"val:{id}"; var cachedValue = await _cache.TryGetAsync(cacheKey); if (cachedValue.HasValue) { resultDictionary[id] = cachedValue.Value; } else { // ID non presente in cache, andrà cercato tramite il servizio EF missingIds.Add(id); } } // STEP 2: Se ci sono cache miss, interroghiamo il servizio EF Core if (missingIds.Any()) { // Riceviamo direttamente un Dictionary ottimizzato da EF Core Dictionary dbResults = await dbController.PODL_getDictOdlPodlAsync(missingIds); // STEP 3: Scriviamo i risultati in cache e li uniamo al dizionario finale foreach (var kvp in dbResults) { var id = kvp.Key; var targetValue = kvp.Value; resultDictionary[id] = targetValue; // Salvataggio atomico e globale su FusionCache var cacheKey = $"val:{id}"; await _cache.SetAsync(cacheKey, targetValue, TimeSpan.FromMinutes(30)); } // STEP 4 [Altamente Consigliato]: Cache Penetration Protection // Se un ID era tra i "missing" ma NON è presente nei risultati del DB, significa che non esiste. // Salviamo un valore sentinella (es. 0 o -1) per evitare di ricontrollare il DB al prossimo giro. foreach (var id in missingIds) { if (!dbResults.ContainsKey(id)) { resultDictionary[id] = 0; // Imposta un default per l'output corrente var cacheKey = $"val:{id}"; await _cache.SetAsync(cacheKey, 0, TimeSpan.FromMinutes(2)); // Scadenza breve per i record inesistenti } } } return resultDictionary; } /// /// Eliminazione record selezionato /// /// /// public async Task POdlDeleteRecord(PODLExpModel currRec) { using var activity = ActivitySource.StartActivity("POdlDeleteRecord"); string source = "DB+REDIS"; var dbResult = await dbController.PODLDeleteRecordAsync(currRec); // elimino cache redis... await FlushFusionCachePOdl(); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"POdlDeleteRecord | Read from {source}: {activity?.Duration.TotalMilliseconds}ms"); return dbResult; } /// /// Avvio fase setup per il record selezionato /// /// /// public async Task POdlDoSetup(PODLExpModel currRec) { using var activity = ActivitySource.StartActivity("POdlDoSetup"); string source = "DB+REDIS"; var dbResult = await dbController.PODL_startSetup(currRec, 0, 1, 1, "", DateTime.Now); // elimino cache redis... await FlushFusionCachePOdl(); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"POdlDoSetup | Read from {source}: {activity?.Duration.TotalMilliseconds}ms"); return dbResult; } /// /// Recupero PODL da chiave /// /// /// public async Task POdlGetByKey(int idxPODL) { string currKey = $"{Utils.redisPOdlByPOdl}:{idxPODL}"; return await GetOrFetchAsync( operationName: "POdlGetByKey", cacheKey: currKey, expiration: TimeSpan.FromMinutes(redisLongTimeCache), fetchFunc: async () => await dbController.PODL_getByKeyAsync(idxPODL) ?? new(), tagList: [Utils.redisPOdlByPOdl] ); } /// /// Recupero PODL da IdxODL /// /// /// public async Task POdlGetByOdlAsync(int idxODL) { string currKey = $"{Utils.redisPOdlByOdl}:{idxODL}"; return await GetOrFetchAsync( operationName: "POdlGetByOdlAsync", cacheKey: currKey, expiration: TimeSpan.FromMinutes(redisLongTimeCache), fetchFunc: async () => await dbController.PODL_getByOdlAsync(idxODL) ?? new(), tagList: [Utils.redisPOdlByOdl] ); } /// /// Effettua il task di eliminazione PODL KIT + istanze + riattivazione PODL originali disattivate tramite stored /// /// IdxPODL parent public async Task PodlIstKitDeleteAsync(int IdxPODL) { using var activity = ActivitySource.StartActivity("PodlIstKitDeleteAsync"); bool fatto = false; // salvo fatto = await dbController.PodlIstKitDeleteAsync(IdxPODL); // svuoto cache await FlushFusionCachePOdl(); activity?.SetTag("data.source", "DB"); return fatto; } /// /// Elenco PODL in un istanza KIT dall'ID del parent /// /// IDX PODL parent /// public async Task> POdlListByKitParentAsync(int IdxPodlParent) { string currKey = $"{Utils.redisPOdlList}_kit:ByParent:{IdxPodlParent}"; return await GetOrFetchAsync( operationName: "POdlListByKitParentAsync", cacheKey: currKey, expiration: GetRandTOut(redisShortTimeCache), fetchFunc: async () => await dbController.ListPODL_ByKitParentAsync(IdxPodlParent) ?? new(), tagList: [Utils.redisPOdlList] ); } /// /// Elenco PODL non avviati filtrati x articolo, KeyRich (che contiene stato) /// /// Solo lanciati (1) o ancora disponibili (0) /// KeyRich (parziale) da cercare (es cod stato x yacht) /// Macchina /// Gruppo /// Data inizio /// Data fine /// public async Task> POdlListGetFiltAsync(bool lanciato, string keyRichPart, string idxMacchina, string codGruppo, DateTime startDate, DateTime endDate) { string currKey = $"{Utils.redisPOdlList}:{codGruppo}:{idxMacchina}:{keyRichPart}:{lanciato}:{startDate:yyyyMMdd_HHmmss}:{endDate:yyyyMMdd_HHmmss}"; return await GetOrFetchAsync( operationName: "POdlListGetFiltAsync", cacheKey: currKey, expiration: GetRandTOut(redisShortTimeCache), fetchFunc: async () => await dbController.ListPODLFiltAsync(lanciato, keyRichPart, idxMacchina, codGruppo, startDate, endDate) ?? new List(), tagList: [Utils.redisPOdlList] ); } /// /// Elenco PODL per composizione KIT (Async) non avviati filtrati x articolo, KeyRich (che contiene stato) /// /// Solo lanciati (1) o ancora disponibili (0) /// KeyRich (parziale) da cercare (es cod stato x yacht) /// Macchina /// Gruppo /// Data inizio /// Data fine /// public async Task> POdlToKitListGetFiltAsync(bool lanciato, string keyRichPart, string idxMacchina, string codGruppo, DateTime startDate, DateTime endDate) { string redisKey = $"{Utils.redisPOdlList}_kit:{codGruppo}:{idxMacchina}:{keyRichPart}:{lanciato}:{startDate:yyyyMMdd_HHmmss}:{endDate:yyyyMMdd_HHmmss}"; return await GetOrFetchAsync( operationName: "POdlToKitListGetFiltAsync", cacheKey: redisKey, expiration: GetRandTOut(redisShortTimeCache * 5), fetchFunc: async () => await dbController.ListPODL_KitFiltAsync( lanciato, keyRichPart, idxMacchina, codGruppo, startDate, endDate ) ?? new List(), tagList: [Utils.redisPOdlList, $"{Utils.redisPOdlList}_kit"] ); } /// /// Chiamata salvataggio ricetta + refresh REDIS /// /// /// /// public async Task POdlUpdateRecipe(int idxPODL, string recipeName) { using var activity = ActivitySource.StartActivity("POdlUpdateRecipe"); string source = "DB"; bool answ = false; answ = await dbController.PODL_updateRecipe(idxPODL, recipeName); // reset redis... await FlushFusionCachePOdl(); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"POdlUpdateRecipe | Read from {source}: {activity?.Duration.TotalMilliseconds}ms"); return answ; } /// /// Aggiornamento record selezionato /// /// /// public async Task POdlUpdateRecord(PODLModel currRec) { using var activity = ActivitySource.StartActivity("POdlUpdateRecord"); string source = "DB"; var dbResult = await dbController.PODLUpdateRecordAsync(currRec); // elimino cache redis... await FlushFusionCachePOdl(); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"POdlUpdateRecord | Read from {source}: {activity?.Duration.TotalMilliseconds}ms"); return dbResult; } /// /// Restituisce le statistiche di processo correnti x depluplica FluxLog /// /// public async Task> ProcFLStatsAsync() { using var activity = ActivitySource.StartActivity("ProcFLStatsAsync"); string source = "REDIS"; List actStats = new List(); string currKey = $"{Utils.redisStatsProcFL}"; // recupero i record statistiche correnti RedisValue rawData = await redisDb.StringGetAsync(currKey); if (rawData.HasValue) { var rawStats = JsonConvert.DeserializeObject>($"{rawData}"); if (rawStats != null) { actStats = rawStats; } } activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"ProcFLStatsAsync | Read from {source}: {activity?.Duration.TotalMilliseconds}ms"); return actStats; } /// /// Ricerca ricetta su MongoDB dato PODL /// /// /// public async Task RecipeGetByPODL(int idxPODL) { RecipeModel? result = null; using var activity = ActivitySource.StartActivity("RecipeGetByPODL"); string source = "MongoDB"; result = await mongoController.RecipeGetByPODL(idxPODL); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"RecipeGetByPODL | Read from {source}: {activity?.Duration.TotalMilliseconds}ms"); return result; } /// /// Salva ricetta su MongoDB /// /// /// public async Task RecipeSetByPODL(RecipeModel currRecord) { using var activity = ActivitySource.StartActivity("RecipeSetByPODL"); string source = "MONGO"; bool answ = false; answ = await mongoController.RecipeSetByPODL(currRecord); await FlushFusionCachePOdl(); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"RecipeSetByPODL | Read from {source}: {activity?.Duration.TotalMilliseconds}ms"); return answ; } /// /// Reset della cache IO post operazioni come setup ODL... /// /// Indirizzo base da cui rimuovere memoria cache /// public async Task ResetIoCache(string baseMem) { using var activity = ActivitySource.StartActivity("ResetIoCache"); string source = "REDIS"; // patterna a partire da cache IO... RedisValue pattern = new RedisValue($"{MpIoNS}:*"); if (!string.IsNullOrEmpty(baseMem)) { pattern = new RedisValue($"{MpIoNS}:{baseMem}:*"); } bool answ = await ExecFlushRedisPatternAsync(pattern); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"ResetIoCache | Read from {source}: {activity?.Duration.TotalMilliseconds}ms"); return answ; } /// /// Stato macchina /// /// /// public async Task StatoMacchinaAsync(string idxMacchina) { string currKey = $"{Utils.redisStatoMacch}:{idxMacchina}"; return await GetOrFetchAsync( operationName: "StatoMacchinaAsync", cacheKey: currKey, expiration: GetRandTOut(redisLongTimeCache), fetchFunc: async () => await dbController.StatoMacchinaAsync(idxMacchina) ?? new(), tagList: [Utils.redisStatoMacch] ); } /// /// Elimina record + svuotamento cache /// /// public async Task TemplateKitDeleteAsync(TemplateKitModel currRecord) { using var activity = ActivitySource.StartActivity("TemplateKitDeleteAsync"); string source = "DB"; bool fatto = false; // salvo fatto = await dbController.TemplateKitDeleteAsync(currRecord); await FlushFusionCacheAsync(Utils.redisKitTempl); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"TemplateKitDeleteAsync | {source} | {activity?.Duration.TotalMilliseconds}ms"); return fatto; } /// /// Elenco Template KIT da ricerca /// /// Codice articolo padre /// Codice articolo figlio /// public async Task> TemplateKitFiltAsync(string codParent, string codChild) { string currKey = $"{Utils.redisKitTempl}:{codParent}:{codChild}"; return await GetOrFetchAsync( operationName: "TemplateKitFiltAsync", cacheKey: currKey, expiration: GetRandTOut(redisLongTimeCache), fetchFunc: async () => await dbController.TemplateKitFiltAsync(codParent, codChild) ?? new List(), tagList: [Utils.redisKitTempl] ); } /// /// Esegue salvataggio record + svuotamento cache /// /// /// public async Task TemplateKitUpsertAsync(TemplateKitModel currRecord, string codAzienda) { using var activity = ActivitySource.StartActivity("TemplateKitUpsertAsync"); string source = "DB"; bool fatto = false; // salvo fatto = await dbController.TemplateKitUpsertAsync(currRecord, codAzienda); await FlushFusionCacheAsync(Utils.redisKitTempl); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"TemplateKitUpsertAsync | {source} | {activity?.Duration.TotalMilliseconds}ms"); return fatto; } /// /// Punteggio compatibilità KIT per KeyFilt indicato /// /// /// /// /// public async Task> TksScoreAsync(string KeyFilt, int MaxResult, bool ForceDb) { string currKey = $"{Utils.redisKitScore}:{KeyFilt}:{MaxResult}"; if (ForceDb) { // Se ForceDb è true, saltiamo il GetOrFetchAsync per forzare il fetch dal DB // e aggiornare la cache. var result = await dbController.TksScoreAsync(KeyFilt, MaxResult) ?? new List(); await _cache.SetAsync(currKey, result, TimeSpan.FromMinutes(redisLongTimeCache), tags: [Utils.redisKitScore]); return result; } return await GetOrFetchAsync( operationName: "TksScoreAsync", cacheKey: currKey, expiration: TimeSpan.FromMinutes(redisLongTimeCache), fetchFunc: async () => await dbController.TksScoreAsync(KeyFilt, MaxResult) ?? new List(), tagList: [Utils.redisKitScore] ); } /// /// Esegue traduzione dato vocabolario da Lingua + Lemma /// /// /// /// public string Traduci(string lemma, string lingua) { if (string.IsNullOrWhiteSpace(lemma)) return string.Empty; if (string.IsNullOrWhiteSpace(lingua)) return lemma; string linguaKey = lingua.ToLowerInvariant().Trim(); string cacheKey = $"vocab:{linguaKey}"; // FusionCache gestisce il lock e recupera l'intero dizionario della lingua. // Se è in L1 (Memory), restituisce l'oggetto C# istantaneamente. // Se non c'è, passa a L2 (Redis) o invoca la factory per caricarlo. var dizionarioLingua = _cache.GetOrSet>( cacheKey, _ => dbController.VocabolarioGetLang(linguaKey), options => options .SetDuration(TimeSpan.FromHours(8)) // Durata logica della cache .SetFailSafe(true, TimeSpan.FromHours(1)) // Se Redis/DB è giù, usa i vecchi dati L1 ); // Ricerca O(1) nel dizionario in memoria if (dizionarioLingua != null && dizionarioLingua.TryGetValue(lemma, out var traduzione)) { return traduzione; } // Fallback: se la parola non è censita, restituisce il lemma originale return lemma; } /// /// Elimina record + svuotamento cache /// /// public async Task WipKitDeleteAsync(WipSetupKitModel currRecord) { using var activity = ActivitySource.StartActivity("WipKitDeleteAsync"); string source = "DB"; bool fatto = false; // salvo fatto = await dbController.WipKitDeleteAsync(currRecord); // svuoto cache await FlushFusionCacheAsync(Utils.redisKitWip); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"WipKitDeleteAsync Read from {source}: {activity?.Duration.TotalMilliseconds}ms"); return fatto; } /// /// Elimina i record più vecchi della data-ora indicata /// /// public async Task WipKitDeleteOlderAsync(DateTime DateLimit) { using var activity = ActivitySource.StartActivity("WipKitDeleteOlderAsync"); string source = "DB"; bool fatto = false; // salvo fatto = await dbController.WipKitDeleteOlderAsync(DateLimit); // svuoto cache KitWip await FlushFusionCacheAsync(Utils.redisKitWip); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"WipKitDeleteOlderAsync Read from {source}: {activity?.Duration.TotalMilliseconds}ms"); return fatto; } /// /// Elenco Template KIT da ricerca /// /// /// public async Task> WipKitFiltAsync(string KeyFilt) { string currKey = $"{Utils.redisKitWip}:{KeyFilt}"; return await GetOrFetchAsync( operationName: "WipKitFiltAsync", cacheKey: currKey, expiration: TimeSpan.FromMinutes(redisLongTimeCache), fetchFunc: async () => await dbController.WipKitFiltAsync(KeyFilt) ?? new List(), tagList: [Utils.redisKitWip] ); } /// /// Esegue salvataggio record + svuotamento cache /// /// public async Task WipKitUpsertAsync(WipSetupKitModel currRecord) { using var activity = ActivitySource.StartActivity("WipKitUpsertAsync"); string source = "DB"; bool fatto = false; // salvo fatto = await dbController.WipKitUpsertAsync(currRecord); // svuoto cache KitWip await FlushFusionCacheAsync(Utils.redisKitWip); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"WipKitUpsertAsync | {source} | {activity?.Duration.TotalMilliseconds}ms"); return fatto; } #endregion Public Methods #region Private Fields /// /// Oggetto per collezione dati Activity (span in Uptrace) /// private static readonly ActivitySource ActivitySource = new ActivitySource("MP.DATA.Tracer"); private static IConfiguration _configuration = null!; private static Logger Log = LogManager.GetCurrentClassLogger(); private readonly IFusionCache _cache; private DateTime _artCacheExpiry = DateTime.MinValue; private Dictionary _configData = new(); private DateTime _dtParamExpiry = DateTime.Now; /// /// Elenco CodArticolo usati in istanza KIT (per verifica eliminabilità) /// private HashSet _listCodArtInKit = new(); /// /// Elenco CodArticolo NON usati (per verifica eliminabilità) /// private HashSet _listCodArtNotUsed = new(); /// /// Elenco CodArticolo usati (per verifica eliminabilità) /// private HashSet _listCodArtUsed = new(); private string MpIoNS = ""; private Random rand = new Random(); /// /// Oggetto per connessione a REDIS /// private ConnectionMultiplexer redisConn = null!; /// /// Oggetto per connessione a REDIS modalità admin (ex flux dati) /// private ConnectionMultiplexer redisConnAdmin = null!; /// /// Oggetto DB redis da impiegare x chiamate R/W /// private IDatabase redisDb = null!; /// /// Durata cache Lunga standard (300 sec) /// private int redisLongTimeCache = 300; /// /// Durata cache Breve standard (5 sec) /// private int redisShortTimeCache = 5; /// /// Soglia minima (ms) per log timing in console /// private double slowLogThresh = 0; private bool traceEnabled = false; #endregion Private Fields #region Private Properties private static MpSpecController dbController { get; set; } = null!; private static MpMongoController mongoController { get; set; } = null!; #endregion Private Properties #region Private Methods /// /// Verifica caricamento dizionario ConfigData /// /// private async Task EnsureConfigLoadedAsync() { if (_configData.Count == 0) { var list = await ConfigGetAllAsync(); _configData = list .GroupBy(x => x.Chiave) .ToDictionary(g => g.Key, g => g.First().Valore); } } /// /// Esegue flush memoria redis dato keyVal, async /// /// /// private async Task ExecFlushRedisPatternAsync(RedisValue pat2Flush) { bool answ = false; using var activity = ActivitySource.StartActivity("ExecFlushRedisPatternAsync"); string source = "REDIS"; var masterEndpoint = redisConn.GetEndPoints() .Where(ep => redisConn.GetServer(ep).IsConnected && !redisConn.GetServer(ep).IsReplica) .FirstOrDefault(); // sepattern è "*" elimino intero DB... if (masterEndpoint != null && (pat2Flush.Equals(new RedisValue("*")) || pat2Flush == RedisValue.Null)) { redisConn.GetServer(masterEndpoint).FlushDatabase(database: redisDb.Database); } else { var server = redisConn.GetServer(masterEndpoint); var keys = server.Keys(database: redisDb.Database, pattern: pat2Flush, pageSize: 1000); var deleteTasks = new List(); 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; activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"ExecFlushRedisPatternAsync | Read from {source}: {activity?.Duration.TotalMilliseconds}ms"); return answ; } private async Task FlushFusionCacheArticoli() { using var activity = ActivitySource.StartActivity("FlushFusionCacheArticoli"); string source = "FUSION"; bool answ = await FlushFusionCacheAsync(new List() { Utils.redisArtList, Utils.redisArtByDossier }); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"FlushFusionCacheArticoli | {source} | {activity?.Duration.TotalMilliseconds}ms"); return answ; } /// /// Cancellazione FusionCache (totale) /// /// private async Task FlushFusionCacheAsync() { await _cache.ClearAsync(allowFailSafe: false); _configData.Clear(); _artCacheExpiry = DateTime.Now.AddHours(-1); return true; } /// /// Cancellazione FusionCache dato singolo tag /// /// private async Task FlushFusionCacheAsync(string tag) { if (string.IsNullOrWhiteSpace(tag)) return false; await _cache.RemoveByTagAsync(tag); _configData.Clear(); return true; } /// /// Cancellazione FusionCache dato elenco tags /// /// private async Task FlushFusionCacheAsync(List listTags) { if (listTags == null || listTags.Count == 0) return false; // Generiamo i Task di rimozione ed eseguiamoli in parallelo su Redis/L1 var tasks = listTags .Where(tag => !string.IsNullOrWhiteSpace(tag)) .Select(tag => _cache.RemoveByTagAsync(tag).AsTask()); await Task.WhenAll(tasks); _configData.Clear(); return true; } private async Task FlushFusionCacheConfig() { using var activity = ActivitySource.StartActivity("FlushFusionCacheConfig"); string source = "FUSION"; bool answ = await FlushFusionCacheAsync(new List() { Utils.redisConfKey }); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"FlushFusionCacheConfig | {source} | {activity?.Duration.TotalMilliseconds}ms"); return answ; } private async Task FlushFusionCacheFluxLog() { using var activity = ActivitySource.StartActivity("FlushFusionCacheFluxLog"); string source = "FUSION"; bool answ = await FlushFusionCacheAsync(new List() { Utils.redisFluxLogFilt, Utils.redisParetoFLKey }); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"FlushFusionCacheFluxLog | {source} | {activity?.Duration.TotalMilliseconds}ms"); return answ; } private async Task FlushFusionCacheKit() { using var activity = ActivitySource.StartActivity("FlushFusionCacheKit"); string source = "FUSION"; bool answ = await FlushFusionCacheAsync(new List() { Utils.redisPOdlList, Utils.redisKitInst, Utils.redisKitWip, Utils.redisKitScore, Utils.redisPOdlByCodArt }); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"FlushFusionCacheKit | {source} | {activity?.Duration.TotalMilliseconds}ms"); return answ; } /// /// Reset macchine e gruppi /// private async Task FlushFusionCacheMacGrp() { using var activity = ActivitySource.StartActivity("FlushFusionCacheMacGrp"); string source = "FUSION"; bool answ = await FlushFusionCacheAsync(new List { Utils.redisAnagGruppi, Utils.redisMacList }); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"FlushFusionCacheMacGrp | {source} | {activity?.Duration.TotalMilliseconds}ms"); return answ; } /// /// Reset cache operatori e gruppi /// private async Task FlushFusionCacheOprGrp() { using var activity = ActivitySource.StartActivity("FlushFusionCacheOprGrp"); string source = "FUSION"; bool answ = await FlushFusionCacheAsync(new List { Utils.redisAnagGruppi, Utils.redisOprList }); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"FlushFusionCacheOprGrp | {source} | {activity?.Duration.TotalMilliseconds}ms"); return answ; } private async Task FlushFusionCachePOdl() { using var activity = ActivitySource.StartActivity("FlushFusionCachePOdl"); string source = "FUSION"; bool answ = await FlushFusionCacheAsync(new List() { Utils.redisXdlData, Utils.redisPOdlByOdl, Utils.redisPOdlByPOdl, Utils.redisPOdlList }); activity?.SetTag("data.source", source); activity?.Stop(); LogTrace($"FlushFusionCachePOdl | {source} | {activity?.Duration.TotalMilliseconds}ms"); return answ; } /// /// Implementa gestione FusionCache+ tracking attività /// - recupero cache da memoria o da obj esterno + cache memoria /// - recupero da fetchFunc se mancasse + store in cache L1/L2 /// /// /// /// /// /// private async Task GetOrFetchAsync(string operationName, string cacheKey, Func> fetchFunc, TimeSpan expiration, params string[] tagList) { using var activity = ActivitySource.StartActivity(operationName); string source; var tryGet = await _cache.TryGetAsync(cacheKey); if (tryGet.HasValue) { source = "MEMORY"; var result = tryGet.Value!; activity?.SetTag("data.source", source); activity?.Stop(); // se supero la soglia loggo... if (activity?.Duration.TotalMilliseconds > slowLogThresh) { LogTrace($"{operationName} | {source} | {activity?.Duration.TotalMilliseconds:F4} ms"); } return result; } bool fromDb = false; // cache in redis var cacheOptions = new FusionCacheEntryOptions() .SetDuration(expiration) .SetFailSafe(true); // cache in RAM per 1/3 del tempo x risparmiare risorse cacheOptions.MemoryCacheDuration = expiration / 3; var final = await _cache.GetOrSetAsync( cacheKey, async _ => { fromDb = true; return await fetchFunc(); }, options: cacheOptions, tags: tagList ); source = fromDb ? "DB" : "REDIS"; activity?.SetTag("data.source", source); activity?.Stop(); // switch log in base a source.. switch (source) { case "DB": LogTrace($"{operationName} | {source} | {activity?.Duration.TotalMilliseconds:F4} ms", reqLevel: NLog.LogLevel.Info); break; case "REDIS": LogTrace($"{operationName} | {source} | {activity?.Duration.TotalMilliseconds:F4} ms", reqLevel: NLog.LogLevel.Debug); break; default: LogTrace($"{operationName} | {source} | {activity?.Duration.TotalMilliseconds:F4} ms"); break; } return final!; } /// /// Restituisce un timeout dal valore secondi richiesti + tempo random +/-3% /// /// /// private TimeSpan GetRandTOut(double durationSec) { double noise = (rand.NextDouble() * 0.06) - 0.03; double rValue = durationSec * (1 + noise); return TimeSpan.FromSeconds(rValue); } /// /// Helper trace messaggio log (SE abilitato) /// /// private void LogTrace(string traceMsg, NLog.LogLevel? reqLevel = null) { if (!traceEnabled) return; reqLevel ??= NLog.LogLevel.Trace; // Loggo! Log.Log(reqLevel, traceMsg); } /// /// Merge statistiche Dedup /// /// /// private async Task ProcDedupStatMergeAsync(List procStats) { List actStats = await ProcFLStatsAsync(); // se fosse vuoto --> add diretto if (actStats.Count == 0) { actStats.AddRange(procStats); } else { // aggiorno su redis i record statistiche 1:1... foreach (var recStat in procStats) { // cerco se ci fosse x aggiornare var currRec = actStats.Where(x => x.IdxMacchina == recStat.IdxMacchina && x.CodFlux == recStat.CodFlux && x.Interval == recStat.Interval && x.Num4Int == recStat.Num4Int).FirstOrDefault(); // se trovato aggiorno if (currRec != null) { currRec.ProcTime += recStat.ProcTime; currRec.NumRec += recStat.NumRec; } // altrimenti aggiungo else { actStats.Add(recStat); } } } // salvo record statistiche var rawData = JsonConvert.SerializeObject(actStats); string currKey = $"{Utils.redisStatsProcFL}"; return await redisDb.StringSetAsync(currKey, rawData); } /// /// Merge statistiche DB Maintenance /// /// /// private async Task RecDbMaintStatAsync(TimeSpan duration) { Dictionary actStats = await DbDedupStatsAsync(); // aggiungo record! actStats.Add(DateTime.Now, duration.TotalSeconds); // salvo NUOVO record statistiche string currKey = $"{Utils.redisStatsDbMaint}"; var rawData = JsonConvert.SerializeObject(actStats); return await redisDb.StringSetAsync(currKey, rawData); } private string RedHashMpIO(string keyName) { string result = keyName; try { result = $"{MpIoNS}:{keyName}".Replace("\\", "_"); } catch (Exception exc) { Log.Error($"Errore in RedHashMpIO{Environment.NewLine}{exc}"); } return result; } #endregion Private Methods } }