Merge remote-tracking branch 'origin/develop'

This commit is contained in:
Samuele Locatelli
2026-04-15 17:39:13 +02:00
317 changed files with 89140 additions and 34847 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.4.33205.214
# Visual Studio Version 18
VisualStudioVersion = 18.4.11620.152 stable
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MP.Data", "MP.Data\MP.Data.csproj", "{A0C7A1E7-6E5F-41BA-8ED0-C4A6C581C1B3}"
EndProject
+1 -3
View File
@@ -1,11 +1,9 @@
using EgwCoreLib.Razor;
using global::Microsoft.AspNetCore.Components;
using Microsoft.EntityFrameworkCore.SqlServer.Query.Internal;
using Microsoft.JSInterop;
using MP.Data.DbModels;
using MP.Data.Services;
using NLog;
using System;
namespace MP_TAB3.Components
{
@@ -480,7 +478,7 @@ namespace MP_TAB3.Components
bool doForce = false;
if (SDService.MachNumPzGet(IdxMaccCur) != CRecMSE.PezziProd || SDService.MachProdStGet(IdxMaccCur).IdxOdl != CRecMSE.IdxOdl || (datiProdAct != null && datiProdAct.PzTotODL != CRecMSE.PezziProd))
{
datiProdAct = await TabDServ.StatoProdMacchina(IdxMaccCur, DateTime.Now);
datiProdAct = await TabDServ.StatoProdMacchinaAsync(IdxMaccCur, DateTime.Now);
SDService.MachProdStSet(IdxMaccCur, datiProdAct);
SDService.MachNumPzSet(IdxMaccCur, CRecMSE.PezziProd);
doForce = true;
+5 -5
View File
@@ -819,7 +819,7 @@ namespace MP_TAB3.Components
}
}
// verifica conferma produzione
datiProdAct = await TabDServ.StatoProdMacchina(IdxMaccSel, DateTime.Now);
datiProdAct = await TabDServ.StatoProdMacchinaAsync(IdxMaccSel, DateTime.Now);
await checkConfProd();
}
@@ -1303,7 +1303,7 @@ namespace MP_TAB3.Components
// var pUpd = Task.Run(async () =>
// {
// // controllo ANCHE che la tav correntemente selezionata NON abbia pezzi da confermare
// datiProdAct = await TabDServ.StatoProdMacchina(IdxMaccMain, adesso);
// datiProdAct = await TabDServ.StatoProdMacchinaAsync(IdxMaccMain, adesso);
// answ = datiProdAct.PzConfBuoni == 0 && datiProdAct.Pz2RecScarto == 0;
// });
// pUpd.Wait();
@@ -1554,7 +1554,7 @@ namespace MP_TAB3.Components
if (isMulti && !needConfProd)
{
// verifica conferma produzione
var datiProdAltra = await TabDServ.StatoProdMacchina(idxMaccAltraTav, DateTime.Now);
var datiProdAltra = await TabDServ.StatoProdMacchinaAsync(idxMaccAltraTav, DateTime.Now);
needConfProd = (datiProdAltra.Pz2RecTot > 0 || datiProdAltra.Pz2RecScarto > 0 || datiProdAltra.Pz2RecRilav > 0) && !isSlave;
}
if (needConfProd)
@@ -1591,7 +1591,7 @@ namespace MP_TAB3.Components
else // se NON sono in setup verifico se ho pz da confermare
{
// recupero pz da confermare
datiProdAct = await TabDServ.StatoProdMacchina(IdxMaccSel, adesso);
datiProdAct = await TabDServ.StatoProdMacchinaAsync(IdxMaccSel, adesso);
await checkConfProd();
@@ -1802,7 +1802,7 @@ namespace MP_TAB3.Components
if (!isMulti || (isMulti && IdxMaccSel.IndexOf("#") > 0))
{
var rawData = await TabDServ.PezziProdMacchina(IdxMaccSel);
datiProdAct = await TabDServ.StatoProdMacchina(IdxMaccSel, DateTime.Now);
datiProdAct = await TabDServ.StatoProdMacchinaAsync(IdxMaccSel, DateTime.Now);
await checkConfProd();
Log.Trace("OdlMan.ReloadData | check checkConfProd done");
}
+1 -1
View File
@@ -225,7 +225,7 @@ namespace MP_TAB3.Components
{
isProcessing = true;
await Task.Delay(1);
datiProdAct = await TabDServ.StatoProdMacchina(IdxMaccSel, dtReqUpdate);
datiProdAct = await TabDServ.StatoProdMacchinaAsync(IdxMaccSel, dtReqUpdate);
// aggiorno visualizzazione...
numPzProdotti = datiProdAct.PzTotODL;
numPz2Rec = datiProdAct.Pz2RecTot;
+1 -1
View File
@@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<Version>6.16.2603.411</Version>
<Version>6.16.2604.1109</Version>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>MP_TAB3</RootNamespace>
</PropertyGroup>
+1 -1
View File
@@ -46,7 +46,7 @@
<script>
Blazor.start({
reconnectionOptions: {
maxRetries: 600,
maxRetries: 3600,
retryIntervalMilliseconds: 1000
},
}).then(() => {
+1 -1
View File
@@ -1,6 +1,6 @@
<body>
<i>Modulo MAPOSPEC </i>
<h4>Versione: 6.16.2603.411</h4>
<h4>Versione: 6.16.2604.1109</h4>
<br /> Note di rilascio:
<ul>
<li>
+1 -1
View File
@@ -1 +1 @@
6.16.2603.411
6.16.2604.1109
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>6.16.2603.411</version>
<version>6.16.2604.1109</version>
<url>https://nexus.steamware.net/repository/SWS/MP-TAB3/stable/LAST/MP-TAB3.zip</url>
<changelog>https://nexus.steamware.net/repository/SWS/MP-TAB3/stable/LAST/ChangeLog.html</changelog>
<mandatory>false</mandatory>
+8 -2
View File
@@ -431,8 +431,14 @@ namespace MP.AppAuth.Services
public void Dispose()
{
// Clear database controller
dbController.Dispose();
MpDbController.Dispose();
if (dbController != null)
{
dbController.Dispose();
}
if (MpDbController != null)
{
MpDbController.Dispose();
}
}
public async Task FlushRedisCache()
+89
View File
@@ -0,0 +1,89 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using static MP.Core.Objects.Enums;
namespace MP.Core.DTO
{
/// <summary>
/// Struttura conf tipo dati
/// </summary>
public class DataConfDto
{
#region Public Properties
/// <summary>
/// Struttura Valore composto a partire da altre variabili read/write esistenti
/// </summary>
public string ComposedVal { get; set; } = "";
/// <summary>
/// Decodifica per tipoMem = BitMap:
/// Array di valori assegnati ai singoli bit
/// </summary>
public List<string> decodeMap { get; set; } = new List<string>();
/// <summary>
/// DESCRIZIONE parametro
/// </summary>
public string description { get; set; } = "";
/// <summary>
/// Valore per determinare Display Ordinal
/// </summary>
public int displOrdinal { get; set; } = 0;
/// <summary>
/// Fattore di MOLTIPLICA per eventuale scalatura
/// es: leggo 1'234 con factor=0.01 --> 12.34, con factor=100 --> 123'400
/// </summary>
public double factor { get; set; } = 1;
/// <summary>
/// Indice nell'area di memoria (da valore iniziale = 0)
/// </summary>
public int index { get; set; } = 0;
/// <summary>
/// Valore massimo ammesso (ove usato, es simulazione)
/// </summary>
public double maxVal { get; set; } = 9999;
/// <summary>
/// Nome "assoluto" della posizione nell'area di memoria (anche diverso da indice)
/// </summary>
public string memAddr { get; set; } = "";
/// <summary>
/// Valore minimo ammesso (ove usato, es simulazione)
/// </summary>
public double minVal { get; set; } = 0;
/// <summary>
/// NOME parametro
/// </summary>
public string name { get; set; } = "none";
/// <summary>
/// Size in byte
/// </summary>
public int size { get; set; } = 0;
/// <summary>
/// Tipo di dato
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public plcDataType tipoMem { get; set; } = plcDataType.Int;
/// <summary>
/// Unità di misura del parametro
/// </summary>
public string unit { get; set; } = "";
/// <summary>
/// Valore parametro (come stringa, decimali con ",", default VUOTO), poi LETTO da PLC (o appena scritto)
/// </summary>
public string value { get; set; } = "";
#endregion Public Properties
}
}
+49
View File
@@ -0,0 +1,49 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using static MP.Core.Objects.Enums;
namespace MP.Core.DTO
{
/// <summary>
/// Struttura conf tipo dati con estensione TimeSeries + VarCalc
/// </summary>
public class DataConfTsVcDto : DataConfDto
{
#region Public Properties
/// <summary>
/// Tipo di funzione da applicare al dato
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public VC_func func { get; set; } = VC_func.MAX;
/// <summary>
/// Indica se il parametro sia semidefinito-positivo (=possa solo crescere) + relativa gestione
/// es: i contatori energia, contatori TOT generali non resettabili ore impiego / pezzi
/// </summary>
public bool onlyIncr { get; set; } = false;
/// <summary>
/// Periodo campionamento
/// </summary>
public int period { get; set; } = 60;
/// <summary>
/// Indica se invio parametro abilitato (false = filtrato, no FluxLog send)
/// </summary>
public bool sendEnabled { get; set; } = true;
/// <summary>
/// Valore della DeadBand per rilevare variazioni in valore assoluto, se nullo non impiegato
/// </summary>
public double? thresholdAbs { get; set; }
/// <summary>
/// Valore della DeadBand per rilevare variazioni in valore percentuale (5% = 0.05), se nullo non impiegato
/// </summary>
public double? thresholdPerc { get; set; }
#endregion Public Properties
}
}
+33
View File
@@ -0,0 +1,33 @@
namespace MP.Core.DTO
{
/// <summary>
/// Tracciato InputEvents in formato JSON valido
/// Derivato da input realtime valore=3&dtEve=20181206180600000&dtCurr=20181206180600000&cnt=999
/// </summary>
public class EvDataDto
{
#region Public Properties
/// <summary>
/// Contatore incrementale x riordino invio (opzionale)
/// </summary>
public int cnt { get; set; } = 0;
/// <summary>
/// DataOra corrente della trasmissione
/// </summary>
public DateTime dtCurr { get; set; } = DateTime.Now;
/// <summary>
/// DataOra evento
/// </summary>
public DateTime dtEve { get; set; } = DateTime.Now;
/// <summary>
/// Valore del dato di flusso registrato
/// </summary>
public string valore { get; set; } = "-";
#endregion Public Properties
}
}
+13
View File
@@ -0,0 +1,13 @@
namespace MP.Core.DTO
{
/// <summary>
/// Tracciato FluxLog in formato JSON valido
/// </summary>
public class FLogDataDto : EvDataDto
{
/// <summary>
/// nome del flusso
/// </summary>
public string flux { get; set; } = "ND";
}
}
+14
View File
@@ -0,0 +1,14 @@
namespace MP.Core.DTO
{
/// <summary>
/// Array valori tipo flogData inviati come JSon
/// </summary>
public class FLogJsonPayloadDto
{
#region Public Properties
public List<FLogDataDto> fluxData { get; set; } = new();
#endregion Public Properties
}
}
+23
View File
@@ -0,0 +1,23 @@
namespace MP.Core.DTO
{
/// <summary>
/// Struttura conf memorie PLC (inizialmente SIEMENS)
/// </summary>
public class PlcMemMapDto
{
#region Public Fields
/// <summary>
/// Parametri ammessi per IOB x lettura
/// </summary>
public Dictionary<string, DataConfTsVcDto> mMapRead = new Dictionary<string, DataConfTsVcDto>();
/// <summary>
/// Parametri ammessi per IOB x scrittura
/// </summary>
public Dictionary<string, DataConfDto> mMapWrite = new Dictionary<string, DataConfDto>();
#endregion Public Fields
}
}
+8
View File
@@ -0,0 +1,8 @@
namespace MP.Core.DTO
{
public class StatDataDTO
{
public string Label { get; set; } = "";
public double Value { get; set; } = 0;
}
}
+32
View File
@@ -0,0 +1,32 @@
namespace MP.Core.DTO
{
/// <summary>
/// Tracciato UserLog in formato JSON valido
/// </summary>
public class ULogDataDto : EvDataDto
{
#region Public Properties
/// <summary>
/// Nome del flusso (RC/RS/DI)
/// </summary>
public string flux { get; set; } = "DI";
/// <summary>
/// [OPZIONALE] label = causale scarto / tagCode
/// </summary>
public string label { get; set; } = "";
/// <summary>
/// Operatore di riferimento
/// </summary>
public int matrOpr { get; set; } = 0;
/// <summary>
/// [OPZIONALE] valNum = esitoOk (0/1) / Quantità di scarto associata
/// </summary>
public int valNum { get; set; } = 0;
#endregion Public Properties
}
}
+14
View File
@@ -0,0 +1,14 @@
namespace MP.Core.DTO
{
/// <summary>
/// Array valori tipo UserLogData inviati come JSon
/// </summary>
public class ULogJsonPayloadDto
{
#region Public Properties
public List<ULogDataDto> fluxData { get; set; }
#endregion Public Properties
}
}
+9
View File
@@ -0,0 +1,9 @@
namespace MP.Core.DTO
{
public class WeightDTO
{
public string Method { get; set; } = "";
public int OldWeight { get; set; } = 100;
public int NewWeight { get; set; } = 0;
}
}
+2
View File
@@ -7,6 +7,8 @@
</PropertyGroup>
<ItemGroup>
<Compile Remove="DTO\ChartDataPointDto.cs" />
<Compile Remove="DTO\ChartSeriesDto.cs" />
<Compile Remove="DTO\FermiCommentatiDTO.cs" />
<Compile Remove="DTO\userTknDTO.cs" />
</ItemGroup>
+156 -31
View File
@@ -1,8 +1,4 @@
#if false
using MP.Core.DbModels;
#endif
using StackExchange.Redis;
using StackExchange.Redis;
namespace MP.Core
{
@@ -34,6 +30,11 @@ namespace MP.Core
public const string redisGiacenzaList = redisBaseAddr + "Cache:GiacenzaList";
public const string redisKit = redisBaseAddr + "Cache:Kit";
public const string redisKitInst = redisBaseAddr + "Cache:Kit:Inst";
public const string redisKitScore = redisBaseAddr + "Cache:Kit:Score";
public const string redisKitTempl = redisBaseAddr + "Cache:Kit:Templ";
public const string redisKitWip = redisBaseAddr + "Cache:Kit:Wip";
public const string redisMacByFlux = redisBaseAddr + "Cache:MacByFlux";
public const string redisMacList = redisBaseAddr + "Cache:MacList";
@@ -45,6 +46,7 @@ namespace MP.Core
public const string redisOdlByBatch = redisXdlData + "OdlByBatch";
public const string redisOdlCurrByMac = redisXdlData + "OdlByMac";
public const string redisOdlLastByMac = redisXdlData + "LastOdlByMac";
public const string redisOdlList = redisXdlData + "OdlList";
@@ -53,18 +55,15 @@ namespace MP.Core
public const string redisParamPageExp = redisBaseAddr + "Cache:ParamPage";
public const string redisParetoFLKey = redisBaseAddr + "Cache:ParetoFL";
public const string redisStatoMacch = redisBaseAddr + "Cache:StatoMacch";
public const string redisPOdlByOdl = redisXdlData + "POdlByOdl";
public const string redisPOdlByCodArt = redisXdlData + "POdlByCodArt";
public const string redisPOdlByOdl = redisXdlData + "POdlByOdl";
public const string redisPOdlByPOdl = redisXdlData + "POdlByPOdl";
public const string redisPOdlList = redisXdlData + "POdlList";
public const string redisRecipeConf = redisBaseAddr + "Cache:Recipe:Conf";
public const string redisRemRebLog = redisBaseAddr + "Cache:RemReb";
public const string redisStatoCom = redisBaseAddr + "Cache:StatoCom";
public const string redisStatoMacch = redisBaseAddr + "Cache:StatoMacch";
public const string redisStatoProd = redisBaseAddr + "Cache:StatoProd";
public const string redisStatsDbMaint = redisBaseAddr + "Stats:DbMaint";
@@ -76,12 +75,6 @@ namespace MP.Core
public const string redisXdlData = redisBaseAddr + "Cache:XDL:";
public const string redisKit = redisBaseAddr + "Cache:Kit";
public const string redisKitInst = redisBaseAddr + "Cache:Kit:Inst";
public const string redisKitScore = redisBaseAddr + "Cache:Kit:Score";
public const string redisKitTempl = redisBaseAddr + "Cache:Kit:Templ";
public const string redisKitWip = redisBaseAddr + "Cache:Kit:Wip";
#endregion Public Fields
#region Public Properties
@@ -100,6 +93,17 @@ namespace MP.Core
#region Public Methods
/// <summary>
/// Effettua la masking di un valore (INT) rispetto ad una BitMask (INT)
/// </summary>
/// <param name="valore">Valore da filtrare</param>
/// <param name="bitMask">BitMask (espresso in valore INT: es 1001 --> 9)</param>
/// <returns></returns>
public static int bMaskInt(int valore, int bitMask)
{
return valore & bitMask;
}
public static string ConvMinToTime(double minutes)
{
// FIXME TODO: da rendere parametrico da appsettings.json...
@@ -116,16 +120,6 @@ namespace MP.Core
return answ;
}
/// <summary>
/// Hash dati STATUS x la macchina specificata
/// </summary>
/// <param name="idxMacchina"></param>
/// <returns></returns>
public static RedisKey dtMaccHash(string idxMacchina)
{
return (RedisKey)$"{redisBaseAddr}DtMac:{idxMacchina}";
}
public static string FormDurata(double durataMinuti)
{
string answ = "";
@@ -146,9 +140,9 @@ namespace MP.Core
/// </summary>
/// <param name="idxFamIn"></param>
/// <returns></returns>
public static RedisKey hSMI(int idxFamIn)
public static RedisKey GetHashSMI(int idxFamIn)
{
return (RedisKey)$"{redisBaseAddr}hSMI:{idxFamIn}";
return (RedisKey)$"{redisBaseAddr}GetHashSMI:{idxFamIn}";
}
/// <summary>
@@ -184,6 +178,30 @@ namespace MP.Core
return (RedisKey)$"MP:Data:{keyName}";
}
/// <summary>
/// Chiave x CurrObjItems macchina
/// </summary>
/// <param name="idxMacchina"></param>
/// <param name="baseAddr">Chiave override per i valori in caso di dati che accedono al dominio dati di un altra app (es: baseAddr x IO legacy)</param>
/// <returns></returns>
public static RedisKey RedKeyCurrObjItems(string idxMacchina, string baseAddr = null)
{
var prefix = (baseAddr ?? redisBaseAddr).TrimEnd(':');
return (RedisKey)$"{prefix}:CurrentParameters:{idxMacchina}";
}
/// <summary>
/// Hash dati STATUS x la macchina specificata
/// </summary>
/// <param name="idxMacchina"></param>
/// <param name="baseAddr">Chiave override per i valori in caso di dati che accedono al dominio dati di un altra app (es: baseAddr x IO legacy)</param>
/// <returns></returns>
public static RedisKey RedKeyDatiMacc(string idxMacchina, string baseAddr = null)
{
var prefix = (baseAddr ?? redisBaseAddr).TrimEnd(':');
return (RedisKey)$"{prefix}:DtMac:{idxMacchina}";
}
/// <summary>
/// Formato RedisKey delal chaive richeista (completa)
/// </summary>
@@ -192,6 +210,114 @@ namespace MP.Core
return (RedisKey)$"{redisBaseAddr}{keyName}";
}
/// <summary>
/// Hash dati MemMap di un IOB
/// </summary>
/// <param name="idxMacchina"></param>
/// <param name="baseAddr">Chiave override per i valori in caso di dati che accedono al dominio dati di un altra app (es: baseAddr x IO legacy)</param>
/// <returns></returns>
public static RedisKey RedKeyIobMemMap(string idxMacchina, string baseAddr = null)
{
var prefix = (baseAddr ?? redisBaseAddr).TrimEnd(':');
return (RedisKey)$"{prefix}:MemMap:{idxMacchina}";
}
/// <summary>
/// Hash dati MemMap di un IOB
/// </summary>
/// <param name="idxMacchina"></param>
/// <param name="baseAddr">Chiave override per i valori in caso di dati che accedono al dominio dati di un altra app (es: baseAddr x IO legacy)</param>
/// <returns></returns>
public static RedisKey RedKeyIobConfYaml(string idxMacchina, string baseAddr = null)
{
var prefix = (baseAddr ?? redisBaseAddr).TrimEnd(':');
return (RedisKey)$"{prefix}:IOB:{idxMacchina}:ConfYaml";
}
/// <summary>
/// Hash dati ultimo FluxLog ricevuto (ex LiveFLogKeyHash)
/// </summary>
/// <param name="idxMacchina"></param>
/// <param name="flog"></param>
/// <param name="baseAddr">Chiave override per i valori in caso di dati che accedono al dominio dati di un altra app (es: baseAddr x IO legacy)</param>
/// <returns></returns>
public static RedisKey RedKeyLastFLog(string idxMacchina, string flog, string baseAddr = null)
{
var prefix = (baseAddr ?? redisBaseAddr).TrimEnd(':');
return (RedisKey)$"{prefix}:FLOG:{idxMacchina}:{flog}";
}
/// <summary>
/// Hash associazione Iob e macchina acquisizione (WIN)
/// </summary>
/// <param name="idxMacchina"></param>
/// <param name="baseAddr"></param>
/// <returns></returns>
public static RedisKey RedKeyMach2Iob(string idxMacchina, string baseAddr = null)
{
var prefix = (baseAddr ?? redisBaseAddr).TrimEnd(':');
return (RedisKey)$"{prefix}:hM2IOB:{idxMacchina}";
}
/// <summary>
/// Hash dati Conf Iob
/// </summary>
/// <param name="idxMacchina"></param>
/// <param name="baseAddr"></param>
/// <returns></returns>
public static RedisKey RedKeyMachIobConf(string idxMacchina, string baseAddr = null)
{
var prefix = (baseAddr ?? redisBaseAddr).TrimEnd(':');
return (RedisKey)$"{prefix}:IOB:{idxMacchina}:MachIobConf";
}
/// <summary>
/// Hash dati Macchine Multi SM Ingressi
/// </summary>
/// <param name="idxMacchina"></param>
/// <param name="baseAddr">Chiave override per i valori in caso di dati che accedono al dominio dati di un altra app (es: baseAddr x IO legacy)</param>
/// <returns></returns>
public static RedisKey RedKeyMsmi(string idxMacchina, string baseAddr = null)
{
var prefix = (baseAddr ?? redisBaseAddr).TrimEnd(':');
return (RedisKey)$"{prefix}:hMSMI:{idxMacchina}";
}
/// <summary>
/// Hash dati countapezzi macchina
/// </summary>
/// <param name="idxMacchina"></param>
/// <param name="baseAddr">Chiave override per i valori in caso di dati che accedono al dominio dati di un altra app (es: baseAddr x IO legacy)</param>
/// <returns></returns>
public static RedisKey RedKeyPzCount(string idxMacchina, string baseAddr = null)
{
var prefix = (baseAddr ?? redisBaseAddr).TrimEnd(':');
return (RedisKey)$"{prefix}:PzCount:{idxMacchina}";
}
/// <summary>
/// Hash dati SAVED (EXE) TASK x la macchina specificata x poter ripristinare in caso di
/// perdita valore WRITE (ex savedTaskHash)
/// </summary>
/// <param name="idxMacchina"></param>
/// <returns></returns>
public static RedisKey RedKeySavedTask2ExeMacc(string idxMacchina, string baseAddr = null)
{
var prefix = (baseAddr ?? redisBaseAddr).TrimEnd(':');
return (RedisKey)$"{prefix}:SavedTask:{idxMacchina}";
}
/// <summary>
/// Hash dati Task2Exe x la macchina specificata (ex exeTaskHash)
/// </summary>
/// <param name="idxMacchina"></param>
/// <param name="baseAddr">Chiave override per i valori in caso di dati che accedono al dominio dati di un altra app (es: baseAddr x IO legacy)</param>
/// <returns></returns>
public static RedisKey RedKeyTask2ExeMacc(string idxMacchina, string baseAddr = null)
{
var prefix = (baseAddr ?? redisBaseAddr).TrimEnd(':');
return (RedisKey)$"{prefix}:ExeTask:{idxMacchina}";
}
/// <summary>
/// Formato RedisValue delal chaive richeista (completa)
/// </summary>
@@ -201,6 +327,5 @@ namespace MP.Core
}
#endregion Public Methods
}
}
File diff suppressed because it is too large Load Diff
+3 -8
View File
@@ -4,19 +4,14 @@ using Microsoft.Extensions.Configuration;
using MP.Core.DTO;
using MP.Core.Objects;
using MP.Data.DbModels;
using MP.Data.DTO;
using MP.Data.Translate;
using NLog;
using Org.BouncyCastle.Crypto;
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using ZXing;
using static EgwCoreLib.Utils.DtUtils;
using static MP.Core.Objects.Enums;
namespace MP.Data.Controllers
{
@@ -531,7 +526,7 @@ namespace MP.Data.Controllers
}
return fatto;
}
/// <summary>
/// Elenco da tabella Config
/// </summary>
@@ -682,7 +677,7 @@ namespace MP.Data.Controllers
}
/// <summary>
/// Effettua salvataggio snapshot parametri (con stored) + svuota eventuale cache redisConn
/// Effettua salvataggio snapshot parametri (con stored) + svuota eventuale cache _redisConn
/// </summary>
/// <param name="idxMacchina">macchina</param>
/// <param name="maxSec">Num massimo secondi per recuperare dati correnti</param>
@@ -705,7 +700,7 @@ namespace MP.Data.Controllers
}
/// <summary>
/// Effettua salvataggio snapshot parametri (con stored) + svuota eventuale cache redisConn
/// Effettua salvataggio snapshot parametri (con stored) + svuota eventuale cache _redisConn
/// </summary>
/// <param name="idxMacchina">macchina</param>
/// <param name="dtMin">Data min x selezione</param>
+37 -7
View File
@@ -27,6 +27,36 @@ namespace MP.Data.Controllers
#region Public Methods
/// <summary>
/// Insert record allarme
/// </summary>
/// <param name="dtRif">Data evento</param>
/// <param name="machineId">Id macchina</param>
/// <param name="memAddress">area memoria</param>
/// <param name="memAddress">indice memoria</param>
/// <param name="memAddress">valore status</param>
/// <param name="memAddress">valore decodificato</param>
/// <returns></returns>
public bool AlarmLogInsert(DateTime dtRif, string machineId, string memAddress, int memIndex, int statusVal, string valDecoded)
{
bool fatto = false;
using (var dbCtx = new MoonProContext(_configuration))
{
var DtRif = new SqlParameter("@DtRif", dtRif);
var MachineId = new SqlParameter("@MachineId", machineId);
var MemAddress = new SqlParameter("@MemAddress", memAddress);
var MemIndex = new SqlParameter("@MemIndex", memIndex);
var StatusVal = new SqlParameter("@StatusVal", statusVal);
var ValDecoded = new SqlParameter("@ValDecoded", valDecoded);
var result = dbCtx
.Database
.ExecuteSqlRaw("EXEC stp_AL_insertQuery @DtRif, @MachineId, @MemAddress, @MemIndex, @StatusVal, @ValDecoded, ", DtRif, MachineId, MemAddress, MemIndex, StatusVal, ValDecoded);
fatto = result != 0;
}
return fatto;
}
/// <summary>
/// Elenco allarmi macchina
/// </summary>
@@ -66,13 +96,13 @@ namespace MP.Data.Controllers
bool fatto = false;
using (var dbCtx = new MoonProContext(_configuration))
{
var AlarmLogId = new SqlParameter("@AlarmLogId", alarmLogId);
var DtAck = new SqlParameter("@DtAck", dtAck);
var UserAck = new SqlParameter("@UserAck", userAck);
var AlarmLogId = new SqlParameter("@MachineId", alarmLogId);
var DtAck = new SqlParameter("@DtRif", dtAck);
var UserAck = new SqlParameter("@MemAddress", userAck);
var result = dbCtx
.Database
.ExecuteSqlRaw("EXEC stp_AL_setAck @AlarmLogId, @DtAck, @UserAck", AlarmLogId, DtAck, UserAck);
.ExecuteSqlRaw("EXEC stp_AL_setAck @MachineId, @DtRif, @MemAddress", AlarmLogId, DtAck, UserAck);
fatto = result != 0;
}
return fatto;
@@ -89,12 +119,12 @@ namespace MP.Data.Controllers
bool fatto = false;
using (var dbCtx = new MoonProContext(_configuration))
{
var AlarmLogId = new SqlParameter("@AlarmLogId", alarmLogId);
var AlarmLogId = new SqlParameter("@MachineId", alarmLogId);
var DtNotify = new SqlParameter("@DtNotify", dtNotify);
var result = dbCtx
.Database
.ExecuteSqlRaw("EXEC stp_AL_setNotify @AlarmLogId, @DtNotify", AlarmLogId, DtNotify);
.ExecuteSqlRaw("EXEC stp_AL_setNotify @MachineId, @DtNotify", AlarmLogId, DtNotify);
fatto = result != 0;
}
return fatto;
@@ -1180,7 +1210,7 @@ namespace MP.Data.Controllers
}
catch (Exception exc)
{
Log.Error($"Eccezione durante OdlCurrByMacc{Environment.NewLine}{exc}");
Log.Error($"Eccezione durante OdlCurrByMaccAsync{Environment.NewLine}{exc}");
}
}
return dbResult;
+10
View File
@@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace MP.Data.DTO
{
public class ChartSeriesDto
{
public string SeriesName { get; set; }
public List<chartJsData.chartJsTSerie> DataPoints { get; set; }
}
}
+24 -11
View File
@@ -34,18 +34,31 @@ namespace MP.Data.DbModels
{
get
{
string answ = "";
DateTime end = DataFine != null ? (DateTime)DataFine : DateTime.Now;
var tsDurata = (end).Subtract((DateTime)DataInizio);
if (DataInizio == null)
return "";
var start = DataInizio.Value;
var end = DataFine ?? DateTime.Now;
var tsDurata = end - start;
if (tsDurata.TotalDays < 1)
{
answ = $"{tsDurata.Hours:00}h {tsDurata.Minutes:00}'";
}
else
{
answ = $"{tsDurata.Days}gg {tsDurata.Hours:00}h";
}
return answ;
return $"{tsDurata.Hours:00}h {tsDurata.Minutes:00}'";
return $"{tsDurata.Days}gg {tsDurata.Hours:00}h";
}
}
[NotMapped]
public TimeSpan DurataMinutiTimeSpan
{
get
{
if (DataInizio == null)
return TimeSpan.FromSeconds(0);
DateTime end = DataFine != null ? (DateTime)DataFine : DateTime.Now;
TimeSpan tsDurata = (end).Subtract((DateTime)DataInizio);
return tsDurata;
}
}
@@ -8,7 +8,7 @@ using System.ComponentModel.DataAnnotations.Schema;
// </Auto-Generated>
namespace MP.Data.DbModels
{
//[Table("DiarioDichiarazioni")]
[Table("DiarioDichiarazioni")]
public partial class RegistroDichiarazioniModel
{
#region Public Properties
+40
View File
@@ -0,0 +1,40 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
// <Auto-Generated>
// This is here so CodeMaid doesn't reorganize this document
// </Auto-Generated>
namespace MP.Data.DbModels
{
[Table("SignalLog")]
public partial class SignalLogModel
{
#region Public Properties
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Column("idxMsg")]
public int IdxMsg { get; set; }
[Column("dtCurr")]
public DateTime? DtCurr { get; set; } = DateTime.Now;
[Column("idxMacchina")]
[MaxLength(50)]
public string IdxMacchina { get; set; } = "NA";
[MaxLength(50)]
[Column("valore")]
public string Valore { get; set; } = "";
[Column("dtEve")]
public DateTime? DtEve { get; set; }
[Column("contatore")]
public int Contatore { get; set; } = 0;
#endregion Public Properties
}
}
@@ -0,0 +1,47 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
// <Auto-Generated>
// This is here so CodeMaid doesn't reorganize this document
// </Auto-Generated>
namespace MP.Data.DbModels.Utils
{
/// <summary>
/// Classe statistiche impiego orario complessive
/// </summary>
[Table("stats_aggr")]
public class StatsAggregatedModel
{
[Key]
public int Id { get; set; }
/// <summary>
/// Ambiente di destinazione IOC/IO (new/old)
/// </summary>
public string Destination { get; set; } = "";
/// <summary>
/// DataOra riferimento
/// </summary>
public DateTime Hour { get; set; }
/// <summary>
/// Num richieste complete/concluse (con ritorno)
/// </summary>
public long RequestCount { get; set; } = 0;
/// <summary>
/// Durata media esecuzione
/// </summary>
public double AvgDuration { get; set; } = 0;
/// <summary>
/// Durata minima esecuzione
/// </summary>
public double MinDuration { get; set; } = 0;
/// <summary>
/// Durata massima esecuzione
/// </summary>
public double MaxDuration { get; set; } = 0;
/// <summary>
/// numero richieste senza risposta (errori)
/// </summary>
public long NoReply { get; set; } = 0;
}
}
@@ -0,0 +1,51 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
// <Auto-Generated>
// This is here so CodeMaid doesn't reorganize this document
// </Auto-Generated>
namespace MP.Data.DbModels.Utils
{
/// <summary>
/// Classe statistiche impiego (orarie) di dettaglio (per envir/tipo)
/// </summary>
[Table("stats_detail")]
public class StatsDetailModel
{
[Key]
public int Id { get; set; }
/// <summary>
/// Ambiente di destinazione IOC/IO (new/old)
/// </summary>
public string Destination { get; set; } = "";
/// <summary>
/// Codice tipo (metodo chiamato)
/// </summary>
public string Type { get; set; } = "";
/// <summary>
/// DataOra riferimento
/// </summary>
public DateTime Hour { get; set; }
/// <summary>
/// Num richieste complete/concluse (con ritorno)
/// </summary>
public long RequestCount { get; set; } = 0;
/// <summary>
/// Durata media esecuzione
/// </summary>
public double AvgDuration { get; set; } = 0;
/// <summary>
/// Durata minima esecuzione
/// </summary>
public double MinDuration { get; set; } = 0;
/// <summary>
/// Durata massima esecuzione
/// </summary>
public double MaxDuration { get; set; } = 0;
/// <summary>
/// numero richieste senza risposta (errori)
/// </summary>
public long NoReply { get; set; } = 0;
}
}
@@ -0,0 +1,111 @@
// <auto-generated />
using System;
using MP.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace MP.Data.Migrations.MoonPro_Utils
{
[DbContext(typeof(MoonPro_UtilsContext))]
[Migration("20260408062523_InitDb")]
partial class InitDb
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.UseCollation("SQL_Latin1_General_CP1_CI_AS")
.HasAnnotation("ProductVersion", "6.0.36")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1);
modelBuilder.Entity("MP.Data.DbModels.Utils.StatsAggregatedModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"), 1L, 1);
b.Property<double>("AvgDuration")
.HasColumnType("float");
b.Property<string>("Destination")
.HasColumnType("nvarchar(450)");
b.Property<DateTime>("Hour")
.HasColumnType("datetime2");
b.Property<double>("MaxDuration")
.HasColumnType("float");
b.Property<double>("MinDuration")
.HasColumnType("float");
b.Property<long>("NoReply")
.HasColumnType("bigint");
b.Property<long>("RequestCount")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("Destination", "Hour")
.IsUnique()
.HasDatabaseName("idx_statsaggr_env_hour")
.HasFilter("[Destination] IS NOT NULL");
b.ToTable("stats_aggr");
});
modelBuilder.Entity("MP.Data.DbModels.Utils.StatsDetailModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"), 1L, 1);
b.Property<double>("AvgDuration")
.HasColumnType("float");
b.Property<string>("Destination")
.HasColumnType("nvarchar(450)");
b.Property<DateTime>("Hour")
.HasColumnType("datetime2");
b.Property<double>("MaxDuration")
.HasColumnType("float");
b.Property<double>("MinDuration")
.HasColumnType("float");
b.Property<long>("NoReply")
.HasColumnType("bigint");
b.Property<long>("RequestCount")
.HasColumnType("bigint");
b.Property<string>("Type")
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("Destination", "Type", "Hour")
.IsUnique()
.HasDatabaseName("idx_statsdet_hour_env_type")
.HasFilter("[Destination] IS NOT NULL AND [Type] IS NOT NULL");
b.ToTable("stats_detail");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,75 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace MP.Data.Migrations.MoonPro_Utils
{
public partial class InitDb : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "stats_aggr",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Destination = table.Column<string>(type: "nvarchar(450)", nullable: true),
Hour = table.Column<DateTime>(type: "datetime2", nullable: false),
RequestCount = table.Column<long>(type: "bigint", nullable: false),
AvgDuration = table.Column<double>(type: "float", nullable: false),
MinDuration = table.Column<double>(type: "float", nullable: false),
MaxDuration = table.Column<double>(type: "float", nullable: false),
NoReply = table.Column<long>(type: "bigint", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_stats_aggr", x => x.Id);
});
migrationBuilder.CreateTable(
name: "stats_detail",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Destination = table.Column<string>(type: "nvarchar(450)", nullable: true),
Type = table.Column<string>(type: "nvarchar(450)", nullable: true),
Hour = table.Column<DateTime>(type: "datetime2", nullable: false),
RequestCount = table.Column<long>(type: "bigint", nullable: false),
AvgDuration = table.Column<double>(type: "float", nullable: false),
MinDuration = table.Column<double>(type: "float", nullable: false),
MaxDuration = table.Column<double>(type: "float", nullable: false),
NoReply = table.Column<long>(type: "bigint", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_stats_detail", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "idx_statsaggr_env_hour",
table: "stats_aggr",
columns: new[] { "Destination", "Hour" },
unique: true,
filter: "[Destination] IS NOT NULL");
migrationBuilder.CreateIndex(
name: "idx_statsdet_hour_env_type",
table: "stats_detail",
columns: new[] { "Destination", "Type", "Hour" },
unique: true,
filter: "[Destination] IS NOT NULL AND [Type] IS NOT NULL");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "stats_aggr");
migrationBuilder.DropTable(
name: "stats_detail");
}
}
}
@@ -0,0 +1,109 @@
// <auto-generated />
using System;
using MP.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace MP.Data.Migrations.MoonPro_Utils
{
[DbContext(typeof(MoonPro_UtilsContext))]
partial class MoonPro_UtilsContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.UseCollation("SQL_Latin1_General_CP1_CI_AS")
.HasAnnotation("ProductVersion", "6.0.36")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1);
modelBuilder.Entity("MP.Data.DbModels.Utils.StatsAggregatedModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"), 1L, 1);
b.Property<double>("AvgDuration")
.HasColumnType("float");
b.Property<string>("Destination")
.HasColumnType("nvarchar(450)");
b.Property<DateTime>("Hour")
.HasColumnType("datetime2");
b.Property<double>("MaxDuration")
.HasColumnType("float");
b.Property<double>("MinDuration")
.HasColumnType("float");
b.Property<long>("NoReply")
.HasColumnType("bigint");
b.Property<long>("RequestCount")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("Destination", "Hour")
.IsUnique()
.HasDatabaseName("idx_statsaggr_env_hour")
.HasFilter("[Destination] IS NOT NULL");
b.ToTable("stats_aggr");
});
modelBuilder.Entity("MP.Data.DbModels.Utils.StatsDetailModel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"), 1L, 1);
b.Property<double>("AvgDuration")
.HasColumnType("float");
b.Property<string>("Destination")
.HasColumnType("nvarchar(450)");
b.Property<DateTime>("Hour")
.HasColumnType("datetime2");
b.Property<double>("MaxDuration")
.HasColumnType("float");
b.Property<double>("MinDuration")
.HasColumnType("float");
b.Property<long>("NoReply")
.HasColumnType("bigint");
b.Property<long>("RequestCount")
.HasColumnType("bigint");
b.Property<string>("Type")
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("Destination", "Type", "Hour")
.IsUnique()
.HasDatabaseName("idx_statsdet_hour_env_type")
.HasFilter("[Destination] IS NOT NULL AND [Type] IS NOT NULL");
b.ToTable("stats_detail");
});
#pragma warning restore 612, 618
}
}
}
+4 -6
View File
@@ -1,9 +1,6 @@
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using MP.Data.DbModels;
using MP.Data.DTO;
using NLog;
#nullable disable
@@ -82,6 +79,7 @@ namespace MP.Data
public virtual DbSet<RegistroScartiKitModel> DbSetRegScartiKit { get; set; }
public virtual DbSet<RegistroDichiarazioniModel> DbSetRegDich { get; set; }
public virtual DbSet<RegCheckModel> DbSetRegWithCheck { get; set; }
public virtual DbSet<SignalLogModel> DbSetSignalLog { get; set; }
public virtual DbSet<ST_Act> DbSetStAct { get; set; }
@@ -197,7 +195,7 @@ namespace MP.Data
.IsRequired()
.HasMaxLength(50);
});
modelBuilder.Entity<ElencoConfermeProdModel>(entity =>
{
entity.HasKey(e => new { e.DataOraConf, e.IdxMacchina });
@@ -601,7 +599,7 @@ namespace MP.Data
});
modelBuilder.Entity<WipSetupKitModel>(entity =>
{
entity.HasKey(e => new { e.KeyFilt, e.CodOrd});
entity.HasKey(e => new { e.KeyFilt, e.CodOrd });
});
OnModelCreatingPartial(modelBuilder);
+128
View File
@@ -0,0 +1,128 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using MP.Data.DbModels.Utils;
using NLog;
using System;
#nullable disable
// <Auto-Generated>
// This is here so CodeMaid doesn't reorganize this document
// </Auto-Generated>
namespace MP.Data
{
public partial class MoonPro_UtilsContext : DbContext
{
#region Private Fields
private static NLog.Logger Log = LogManager.GetCurrentClassLogger();
private IConfiguration _configuration;
#endregion Private Fields
#region Public Constructors
/// <summary>
/// Indispensabile x prima generazione migrations EFCore
/// </summary>
[Obsolete("This constructor should never be used directly, and is only needed to generate entityframework stuff. Connection string can be adapted as pleased.")]
public MoonPro_UtilsContext()
{
}
//public MoonPro_UtilsContext(IConfiguration configuration)
//{
// _configuration = configuration;
//}
public MoonPro_UtilsContext(DbContextOptions<MoonPro_UtilsContext> options) : base(options)
{
}
#endregion Public Constructors
#region Public Properties
public virtual DbSet<StatsDetailModel> DbSetStatsDet { get; set; }
public virtual DbSet<StatsAggregatedModel> DbSetStatsAggr { get; set; }
#endregion Public Properties
#region Private Methods
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
#endregion Private Methods
#region Protected Methods
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
string connString = _configuration.GetConnectionString("MP.Utils");
if (!string.IsNullOrEmpty(connString))
{
optionsBuilder.UseSqlServer(connString);
}
else
{
optionsBuilder.UseSqlServer("Server=SQL2016DEV;Database=MoonPro_Utils;Trusted_Connection=True;");
}
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasAnnotation("Relational:Collation", "SQL_Latin1_General_CP1_CI_AS");
// indici tabelle stats
modelBuilder.Entity<StatsDetailModel>()
.HasIndex(x => new { x.Destination, x.Type, x.Hour })
.HasDatabaseName("idx_statsdet_hour_env_type")
.IsUnique();
modelBuilder.Entity<StatsAggregatedModel>()
.HasIndex(x => new { x.Destination, x.Hour })
.HasDatabaseName("idx_statsaggr_env_hour")
.IsUnique();
// modelBuilder.Entity<AnagArticoliModel>(entity =>
// {
// entity.HasKey(e => e.CodArticolo);
// entity.ToView("AnagArticoli");
// entity.Property(e => e.CodArticolo)
// .IsRequired()
// .HasMaxLength(50);
// entity.Property(e => e.DescArticolo)
// .IsRequired()
// .HasMaxLength(250);
// entity.Property(e => e.Disegno)
// .IsRequired()
// .HasMaxLength(50);
// entity.Property(e => e.Tipo)
// .IsRequired()
// .HasMaxLength(50);
// entity.Property(e => e.Azienda)
// .IsRequired()
// .HasMaxLength(50);
// });
// modelBuilder.Entity<ParetoFluxLogDTO>(entity =>
// {
// entity.HasKey(e => new { e.IdxMacchina, e.CodFlux });
// });
OnModelCreatingPartial(modelBuilder);
}
#endregion Protected Methods
}
}
+47
View File
@@ -0,0 +1,47 @@
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;
namespace MP.Data.Repository
{
public abstract class BaseRepository : IBaseRepository
{
#region Protected Fields
protected readonly IDbContextFactory<MoonPro_UtilsContext> _ctxFactory;
#endregion Protected Fields
#region Protected Constructors
protected BaseRepository(IDbContextFactory<MoonPro_UtilsContext> ctxFactory)
=> _ctxFactory = ctxFactory;
#endregion Protected Constructors
#region Protected Methods
/// <summary>
/// Creazione dbcontext per singola transazione
/// </summary>
/// <returns></returns>
protected async Task<MoonPro_UtilsContext> CreateContextAsync()
=> await _ctxFactory.CreateDbContextAsync();
#endregion Protected Methods
#if false
/// <summary>
/// Salvataggio dati asincrono
/// </summary>
/// <returns></returns>
protected async Task<bool> SaveChangesAsync(DataLayerContext ctx)
=> await ctx.SaveChangesAsync() > 0;
#endif
#if false
protected readonly DataLayerContext _dbCtx;
protected BaseRepository(DataLayerContext db) => _dbCtx = db;
public async Task<bool> SaveChangesAsync() => await _dbCtx.SaveChangesAsync() > 0;
#endif
}
}
+8
View File
@@ -0,0 +1,8 @@
namespace MP.Data.Repository
{
public interface IBaseRepository
{
//Task<DataLayerContext> CreateContextAsync();
//Task<bool> SaveChangesAsync(DataLayerContext ctx);
}
}
@@ -0,0 +1,36 @@
using EgwCoreLib.Utils;
using MP.Data.DbModels.Utils;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace MP.Data.Repository.Utils
{
/// <summary>
/// Interfaccia per le statistiche aggregate.
/// </summary>
public interface IStatsAggrRepository
{
/// <summary>
/// Recupera l'elenco delle statistiche aggregate per un periodo specifico.
/// </summary>
/// <param name="dtStart">La data di inizio del periodo.</param>
/// <param name="dtEnd">La data di fine del periodo.</param>
/// <returns>L'elenco delle statistiche aggregate ordinate cronologicamente.</returns>
Task<List<StatsAggregatedModel>> GetFiltAsync(DateTime dtStart, DateTime dtEnd);
/// <summary>
/// Recupera l'intervallo temporale disponibile nel database per le statistiche aggregate.
/// </summary>
/// <returns>L'intervallo di date (minima e massima ora presente).</returns>
Task<DtUtils.Periodo> GetRangeAsync();
/// <summary>
/// Inserisce o aggiorna in blocco le statistiche aggregate nel database.
/// </summary>
/// <param name="listRecords">L'elenco dei record da inserire.</param>
/// <param name="removeOld">Se true, elimina preventivamente i record nel periodo richiesto.</param>
/// <returns>Il numero di record inseriti.</returns>
Task<int> UpsertManyAsync(List<StatsAggregatedModel> listRecords, bool removeOld);
}
}
@@ -0,0 +1,55 @@
using EgwCoreLib.Utils;
using MP.Data.DbModels.Utils;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace MP.Data.Repository.Utils
{
/// <summary>
/// Interfaccia per le statistiche di dettaglio.
/// </summary>
public interface IStatsDetailRepository
{
#region Public Methods
/// <summary>
/// Recupera l'elenco delle statistiche di dettaglio per un periodo specifico, con filtri opzionali.
/// </summary>
/// <param name="dtStart">La data di inizio del periodo.</param>
/// <param name="dtEnd">La data di fine del periodo.</param>
/// <param name="sEnvir">Filtro opzionale per l'ambiente (es. "DEV", "PROD").</param>
/// <param name="sType">Filtro opzionale per il tipo di statistica.</param>
/// <returns>L'elenco delle statistiche di dettaglio ordinate cronologicamente.</returns>
Task<List<StatsDetailModel>> GetFiltAsync(DateTime dtStart, DateTime dtEnd, string sEnvir = "", string sType = "");
/// <summary>
/// Elenco record in modalità pareto
/// </summary>
/// <param name="dtStart"></param>
/// <param name="dtEnd"></param>
/// <param name="maxRec"></param>
/// <param name="sDest"></param>
/// <param name="sType"></param>
/// <returns></returns>
Task<List<StatsDetailModel>> GetParetoAsync(DateTime dtStart, DateTime dtEnd, int maxRec, string sDest = "", string sType = "");
/// <summary>
/// Recupera l'intervallo temporale disponibile nel database per le statistiche di dettaglio.
/// </summary>
/// <param name="sEnvir">Filtro opzionale per l'ambiente.</param>
/// <param name="sType">Filtro opzionale per il tipo.</param>
/// <returns>L'intervallo di date (minima e massima ora presente).</returns>
Task<DtUtils.Periodo> GetRangeAsync(string sEnvir, string sType);
/// <summary>
/// Inserisce o aggiorna in blocco le statistiche di dettaglio nel database.
/// </summary>
/// <param name="listRecords">L'elenco dei record da inserire.</param>
/// <param name="removeOld">Se true, elimina preventivamente i record nel periodo richiesto.</param>
/// <returns>Il numero di record inseriti.</returns>
Task<int> UpsertManyAsync(List<StatsDetailModel> listRecords, bool removeOld);
#endregion Public Methods
}
}
@@ -0,0 +1,196 @@
using EgwCoreLib.Utils;
using Microsoft.EntityFrameworkCore;
using MP.Data.DbModels.Utils;
using NLog;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace MP.Data.Repository.Utils
{
public class StatsAggrRepository : BaseRepository, IStatsAggrRepository
{
#region Public Constructors
public StatsAggrRepository(IDbContextFactory<MoonPro_UtilsContext> ctxFactory) : base(ctxFactory)
{
}
#endregion Public Constructors
#region Public Methods
/// <inheritdoc />
public async Task<List<StatsAggregatedModel>> GetFiltAsync(DateTime dtStart, DateTime dtEnd)
{
await using var dbCtx = await CreateContextAsync();
return await dbCtx
.DbSetStatsAggr
.Where(x => x.Hour >= dtStart && x.Hour <= dtEnd)
.AsNoTracking()
.OrderBy(x => x.Hour)
.ToListAsync();
}
/// <inheritdoc />
public async Task<DtUtils.Periodo> GetRangeAsync()
{
await using var dbCtx = await CreateContextAsync();
DtUtils.Periodo answ = new DtUtils.Periodo(DtUtils.PeriodSet.Today);
var query = dbCtx.DbSetStatsAggr.AsQueryable();
var minHour = await query.MinAsync(x => x.Hour);
var maxHour = await query.MaxAsync(x => x.Hour);
answ.Inizio = minHour;
answ.Fine = maxHour;
// ritorno!
return answ;
}
/// <inheritdoc />
public async Task<int> UpsertManyAsync(List<StatsAggregatedModel> listRecords, bool removeOld)
{
if (listRecords == null || !listRecords.Any()) return 0;
int ans = 0;
await using var dbCtx = await CreateContextAsync();
await using var tx = await dbCtx.Database.BeginTransactionAsync();
try
{
// 1. Calcolo del range temporale della lista in arrivo per limitare la query di ricerca
var minHour = listRecords.Min(x => x.Hour);
var maxHour = listRecords.Max(x => x.Hour);
// 2. Se removeOld è true, manteniamo la logica originale (Eliminazione distruttiva)
if (removeOld)
{
// uso direttamente ExecuteDelete quando in EFCore8...
#if false
await dbCtx
.DbSetStatsAggr
.Where(x => x.Hour >= startDate && x.Hour <= endDate)
.ExecuteDeleteAsync();
#endif
var itemsToRemove = await dbCtx.DbSetStatsAggr
.Where(x => x.Hour >= minHour && x.Hour <= maxHour)
.ToListAsync();
if (itemsToRemove.Any())
{
dbCtx.DbSetStatsAggr.RemoveRange(itemsToRemove);
await dbCtx.SaveChangesAsync(); // Commit parziale per la cancellazione
}
}
// 3. LOGICA DI UPSERT (Merge)
// Recuperiamo tutti i record esistenti nel database che cadono nello stesso range temporale
// Questo ci permette di confrontare ciò che arriva con ciò che è già presente.
var existingRecords = await dbCtx.DbSetStatsAggr
.Where(x => x.Hour >= minHour && x.Hour <= maxHour)
.ToListAsync();
// Creiamo un dizionario per ricerca rapida O(1) basato sulla chiave univoca (Dest + Hour)
// Usiamo una Tupla come chiave del dizionario
var lookup = existingRecords.ToDictionary(
x => (x.Destination, x.Hour),
x => x
);
foreach (var incoming in listRecords)
{
var key = (incoming.Destination, incoming.Hour);
if (lookup.TryGetValue(key, out var existing))
{
// --- CASO: UPDATE ---
existing.RequestCount = incoming.RequestCount;
existing.AvgDuration = incoming.AvgDuration;
existing.MinDuration = incoming.MinDuration;
existing.MaxDuration = incoming.MaxDuration;
// Aggiungi altri campi se presenti (es. NoReply, ecc.)
}
else
{
// --- CASO: INSERT ---
await dbCtx.DbSetStatsAggr.AddAsync(incoming);
}
}
// 4. Salvataggio finale
ans = await dbCtx.SaveChangesAsync();
// Commit della transazione
await tx.CommitAsync();
// Pulizia memoria per evitare che il ChangeTracker diventi troppo pesante nei loop lunghi
dbCtx.ChangeTracker.Clear();
return ans;
}
catch (Exception ex)
{
await tx.RollbackAsync();
Log.Error(ex, "Error during UpsertManyAsync");
throw;
}
}
#if false
/// <inheritdoc />
public async Task<int> UpsertManyAsyncOrig(List<StatsAggregatedModel> listRecords, bool removeOld)
{
int answ = 0;
await using var dbCtx = await CreateContextAsync();
await using var tx = await dbCtx.Database.BeginTransactionAsync();
try
{
// in primis se richiesto calcolo range periodo e svuoto...
if (removeOld)
{
var firstRec = listRecords.OrderBy(x => x.Hour).FirstOrDefault();
var lastRec = listRecords.OrderByDescending(x => x.Hour).FirstOrDefault();
if (firstRec != null && lastRec != null)
{
DateTime startDate = firstRec.Hour;
DateTime endDate = lastRec.Hour;
var items = await dbCtx.DbSetStatsAggr
.Where(x => x.Hour >= startDate && x.Hour <= endDate)
.ToListAsync();
dbCtx.DbSetStatsAggr.RemoveRange(items);
await dbCtx.SaveChangesAsync();
}
}
await dbCtx
.DbSetStatsAggr
.AddRangeAsync(listRecords);
// salvo!
answ = await dbCtx.SaveChangesAsync();
// commit transazione
await tx.CommitAsync();
// libero memoria del changeTracker
dbCtx.ChangeTracker.Clear();
return answ;
}
catch
{
await tx.RollbackAsync();
throw;
}
}
#endif
#endregion Public Methods
#region Protected Fields
protected static NLog.Logger Log = LogManager.GetCurrentClassLogger();
#endregion Protected Fields
}
}
@@ -0,0 +1,249 @@
using EgwCoreLib.Utils;
using Microsoft.EntityFrameworkCore;
using MP.Data.DbModels.Utils;
using NLog;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace MP.Data.Repository.Utils
{
public class StatsDetailRepository : BaseRepository, IStatsDetailRepository
{
#region Public Constructors
public StatsDetailRepository(IDbContextFactory<MoonPro_UtilsContext> ctxFactory) : base(ctxFactory)
{
}
#endregion Public Constructors
#region Public Methods
/// <inheritdoc />
public async Task<List<StatsDetailModel>> GetFiltAsync(DateTime dtStart, DateTime dtEnd, string sEnvir = "", string sType = "")
{
await using var dbCtx = await CreateContextAsync();
List<StatsDetailModel> answ = new List<StatsDetailModel>();
// recupero ed ordino per data-ora
var query = dbCtx.DbSetStatsDet
.Where(x => x.Hour >= dtStart && x.Hour <= dtEnd);
if (!string.IsNullOrEmpty(sEnvir))
query = query.Where(x => x.Destination == sEnvir);
if (!string.IsNullOrEmpty(sType))
query = query.Where(x => x.Type == sType);
answ = await query
.AsNoTracking()
.OrderBy(x => x.Hour)
.ThenBy(x => x.Destination)
.ThenBy(x => x.Type)
.ToListAsync();
return answ;
}
/// <inheritdoc />
public async Task<List<StatsDetailModel>> GetParetoAsync(DateTime dtStart, DateTime dtEnd, int maxRec, string sDest = "", string sType = "")
{
await using var dbCtx = await CreateContextAsync();
List<StatsDetailModel> answ = new List<StatsDetailModel>();
// recupero ed ordino per data-ora
var query = dbCtx.DbSetStatsDet
.Where(x => x.Hour >= dtStart && x.Hour <= dtEnd);
if (!string.IsNullOrEmpty(sDest))
query = query.Where(x => x.Destination == sDest);
if (!string.IsNullOrEmpty(sType))
query = query.Where(x => x.Type == sType);
answ = await query
.AsNoTracking()
.OrderByDescending(x => x.RequestCount)
.ThenBy(x => x.Hour)
.ThenBy(x => x.Destination)
.ThenBy(x => x.Type)
.Take(maxRec)
.ToListAsync();
return answ;
}
/// <inheritdoc />
public async Task<DtUtils.Periodo> GetRangeAsync(string sEnvir, string sType)
{
await using var dbCtx = await CreateContextAsync();
DtUtils.Periodo answ = new DtUtils.Periodo(DtUtils.PeriodSet.Today);
var query = dbCtx.DbSetStatsDet.AsQueryable();
if (!string.IsNullOrEmpty(sEnvir))
query = query.Where(x => x.Destination == sEnvir);
if (!string.IsNullOrEmpty(sType))
query = query.Where(x => x.Type == sType);
var minHour = await query.MinAsync(x => x.Hour);
var maxHour = await query.MaxAsync(x => x.Hour);
answ.Inizio = minHour;
answ.Fine = maxHour;
return answ;
}
/// <inheritdoc />
public async Task<int> UpsertManyAsync(List<StatsDetailModel> listRecords, bool removeOld)
{
if (listRecords == null || !listRecords.Any()) return 0;
int answ = 0;
await using var dbCtx = await CreateContextAsync();
await using var tx = await dbCtx.Database.BeginTransactionAsync();
try
{
// 1. Calcolo del range temporale basato sulla lista in arrivo per ottimizzare la query SQL
var minHour = listRecords.Min(x => x.Hour);
var maxHour = listRecords.Max(x => x.Hour);
// 2. Gestione eliminazione distruttiva (se richiesto)
if (removeOld)
{
// uso direttamente ExecuteDelete quando in EFCore8...
#if false
await dbCtx
.DbSetStatsDet
.Where(x => x.Hour >= startDate && x.Hour <= endDate)
.ExecuteDeleteAsync();
#endif
var itemsToRemove = await dbCtx.DbSetStatsDet
.Where(x => x.Hour >= minHour && x.Hour <= maxHour)
.ToListAsync();
if (itemsToRemove.Any())
{
dbCtx.DbSetStatsDet.RemoveRange(itemsToRemove);
await dbCtx.SaveChangesAsync();
}
}
// 3. LOGICA DI UPSERT (Merge basato su Destination + Type + Hour)
// Recuperiamo dal DB solo i record che rientrano nel range temporale della lista in arrivo
var existingRecords = await dbCtx.DbSetStatsDet
.Where(x => x.Hour >= minHour && x.Hour <= maxHour)
.ToListAsync();
// Creiamo il dizionario di lookup con la chiave composta (Dest, Type, Hour)
var lookup = existingRecords.ToDictionary(
x => (x.Destination, x.Type, x.Hour),
x => x
);
foreach (var incoming in listRecords)
{
// Creiamo la chiave di ricerca basata sul record in arrivo
var key = (incoming.Destination, incoming.Type, incoming.Hour);
if (lookup.TryGetValue(key, out var existing))
{
// --- CASO: UPDATE ---
// Aggiorniamo i valori del record esistente con quelli nuovi
existing.RequestCount = incoming.RequestCount;
existing.AvgDuration = incoming.AvgDuration;
existing.MinDuration = incoming.MinDuration;
existing.MaxDuration = incoming.MaxDuration;
existing.NoReply = incoming.NoReply;
}
else
{
// --- CASO: INSERT ---
// Il record non esiste per questa combinazione, lo aggiungiamo
await dbCtx.DbSetStatsDet.AddAsync(incoming);
}
}
// 4. Salvataggio finale delle modifiche (Insert + Update)
answ = await dbCtx.SaveChangesAsync();
// Commit della transazione
await tx.CommitAsync();
// Pulizia memoria del ChangeTracker per evitare accumuli durante sessioni lunghe
dbCtx.ChangeTracker.Clear();
return answ;
}
catch (Exception ex)
{
await tx.RollbackAsync();
Log.Error(ex, "Error during StatsDetail UpsertManyAsync");
throw;
}
}
#endregion Public Methods
#if false
/// <inheritdoc />
public async Task<int> UpsertManyAsyncOrig(List<StatsDetailModel> listRecords, bool removeOld)
{
int answ = 0;
await using var dbCtx = await CreateContextAsync();
await using var tx = await dbCtx.Database.BeginTransactionAsync();
try
{
// in primis se richiesto calcolo range periodo e svuoto...
if (removeOld)
{
var firstRec = listRecords.OrderBy(x => x.Hour).FirstOrDefault();
var lastRec = listRecords.OrderByDescending(x => x.Hour).FirstOrDefault();
if (firstRec != null && lastRec != null)
{
DateTime startDate = firstRec.Hour;
DateTime endDate = lastRec.Hour;
var items = await dbCtx.DbSetStatsDet
.Where(x => x.Hour >= startDate && x.Hour <= endDate)
.ToListAsync();
dbCtx.DbSetStatsDet.RemoveRange(items);
await dbCtx.SaveChangesAsync();
}
}
await dbCtx
.DbSetStatsDet
.AddRangeAsync(listRecords);
// salvo!
answ = await dbCtx.SaveChangesAsync();
// commit transazione
await tx.CommitAsync();
// libero memoria del changeTracker
dbCtx.ChangeTracker.Clear();
return answ;
}
catch
{
await tx.RollbackAsync();
throw;
}
}
#endif
#region Protected Fields
protected static NLog.Logger Log = LogManager.GetCurrentClassLogger();
#endregion Protected Fields
}
}
+196 -39
View File
@@ -1,5 +1,4 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using MP.Core.Objects;
using Newtonsoft.Json;
using NLog;
@@ -8,7 +7,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
namespace MP.Data.Services
@@ -16,39 +15,20 @@ namespace MP.Data.Services
/// <summary>
/// Classe di partenza x costruzione servizi di accesso dati + cache
/// </summary>
public class BaseServ:IDisposable
public class BaseServ : IDisposable
{
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private bool _disposed = false;
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Free managed resources here
redisDb = null;
}
// Free unmanaged resources here
_disposed = true;
}
}
#region Public Constructors
public BaseServ(IConfiguration configuration, IConnectionMultiplexer redConn)
{
_configuration = configuration;
// Verifica conf trace...
_traceEnabled = _configuration.GetValue<bool>("Otel:EnableTracing", false);
// setup componenti REDIS
this.redisConn = redConn;
redisDb = redisConn.GetDatabase();
_redisConn = redConn;
_redisDb = _redisConn.GetDatabase();
// json serializer... FIX errore loop circolare https://www.ryadel.com/en/jsonserializationexception-self-referencing-loop-detected-error-fix-entity-framework-asp-net-core/
JSSettings = new JsonSerializerSettings()
@@ -64,6 +44,12 @@ namespace MP.Data.Services
#region Public Methods
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Recupero info IOB x TAB (da info registrate IOB-WIN--&gt; MP-IO)
/// </summary>
@@ -75,9 +61,9 @@ namespace MP.Data.Services
Stopwatch sw = new Stopwatch();
sw.Start();
IOB_data? result = new IOB_data();
// cerco in redisConn...
// cerco in _redisConn...
string currKey = RedHashMpIO($"hM2IOB:{IdxMacchina}");
RedisValue rawData = redisDb.StringGet(currKey);
RedisValue rawData = _redisDb.StringGet(currKey);
//if (!string.IsNullOrEmpty($"{rawData}"))
if (rawData.HasValue)
{
@@ -87,7 +73,7 @@ namespace MP.Data.Services
else
{
Log.Error($"Errore: non trovato valore <IOB_data> valido in REDIS | key: {currKey}");
Log.Info($"REDIS | conf: {redisConn.Configuration}");
Log.Info($"REDIS | conf: {_redisConn.Configuration}");
Log.Info($" --> Valore trovato:{Environment.NewLine}{rawData}");
}
if (result == null)
@@ -111,11 +97,11 @@ namespace MP.Data.Services
Stopwatch sw = new Stopwatch();
sw.Start();
Dictionary<string, string> result = new Dictionary<string, string>();
// cerco in redisConn...
// cerco in _redisConn...
string currKey = RedHashMpIO($"IOB:{IdxMacchina}:MachIobConf");
try
{
result = redisDb
result = _redisDb
.HashGetAll(currKey)
.ToDictionary(x => $"{x.Name}", x => $"{x.Value}");
source = "REDIS";
@@ -145,11 +131,11 @@ namespace MP.Data.Services
Stopwatch sw = new Stopwatch();
sw.Start();
string result = "";
// cerco in redisConn...
// cerco in _redisConn...
string currKey = RedHashMpIO($"IOB:{IdxMacchina}:ConfYaml");
try
{
result = redisDb.StringGet(currKey);
result = _redisDb.StringGet(currKey);
source = "REDIS";
}
catch (Exception exc)
@@ -170,20 +156,31 @@ namespace MP.Data.Services
#region Protected Fields
/// <summary>
/// Oggetto per collezione dati Activity (span in Uptrace)
/// </summary>
protected static readonly ActivitySource ActivitySource = new ActivitySource("MP.IOC");
protected static IConfiguration _configuration = null!;
protected JsonSerializerSettings? JSSettings;
protected string MpIoNS = "";
/// <summary>
/// Abilitazione operazioni tracing generiche
/// </summary>
protected readonly bool _traceEnabled = false;
/// <summary>
/// Oggetto per connessione a REDIS
/// </summary>
protected IConnectionMultiplexer redisConn = null!;
protected IConnectionMultiplexer _redisConn = null!;
//ISubscriber sub = redisConn.GetSubscriber();
//ISubscriber sub = _redisConn.GetSubscriber();
/// <summary>
/// Oggetto DB redis da impiegare x chiamate R/W
/// </summary>
protected IDatabase redisDb = null!;
protected IDatabase _redisDb = null!;
protected JsonSerializerSettings? JSSettings;
protected string MpIoNS = "";
#endregion Protected Fields
@@ -225,6 +222,128 @@ namespace MP.Data.Services
#region Protected Methods
/// <summary>
/// Helper avvio attività per la funzione tracciata
/// </summary>
/// <param name="methodName"></param>
/// <returns></returns>
protected static Activity? StartActivity([CallerMemberName] string? methodName = null)
{
var activity = ActivitySource.StartActivity(methodName ?? "UNDEF");
activity?.SetTag("host.name", Environment.MachineName);
return activity;
}
/// <summary>
/// Invalida una o più chiavi/pattern in Redis
/// </summary>
protected async Task ClearCacheAsync(params string[] patterns)
{
foreach (var pattern in patterns)
{
// Chiamata al tuo metodo esistente
await ExecFlushRedisPatternAsync((RedisValue)pattern);
}
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Free managed resources here
_redisDb = null;
}
// Free unmanaged resources here
_disposed = true;
}
}
/// <summary>
/// Metodo di flush dati cache Redis
/// </summary>
/// <param name="pattern"></param>
/// <returns></returns>
protected async Task ExecFlushRedisPatternAsync(RedisValue pattern)
{
// Qui inserisci la tua logica attuale (es. via Lua script o Keys/Scan)
// Esempio rapido via server scan:
var endpoints = _redisConn.GetEndPoints();
foreach (var endpoint in endpoints)
{
var server = _redisConn.GetServer(endpoint);
await foreach (var key in server.KeysAsync(_redisDb.Database, pattern))
{
await _redisDb.KeyDeleteAsync(key);
}
}
}
/// <summary>
/// Helper generale di lettura da cache o da funzione (DB) con caching successivo
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key"></param>
/// <param name="factory"></param>
/// <param name="expiration"></param>
/// <returns></returns>
protected async Task<T> GetOrSetCacheAsync<T>(string key, Func<Task<T>> factory, TimeSpan? expiration = null, [CallerMemberName] string? caller = null)
{
using var activity = StartActivity();
string source = "DB";
// 1. Provo Redis
var cached = await _redisDb.StringGetAsync(key);
if (cached.HasValue)
{
source = "REDIS";
var cachedResult = JsonConvert.DeserializeObject<T>(cached!)!;
activity?.SetTag("data.source", source);
LogTrace($"{source} | trace: {activity?.TraceId} | {activity?.Duration.TotalMilliseconds}ms", NLog.LogLevel.Trace, caller);
return cachedResult;
}
// 2. Chiamo il factory (DB)
T result = await factory();
if (result != null)
{
// 3. Salva in Redis per la prossima volta
var serialized = JsonConvert.SerializeObject(result, JSSettings);
await _redisDb.StringSetAsync(key, serialized, expiration ?? LongCache);
}
// sistemo activity tracking data
activity?.SetTag("data.source", source);
activity?.Stop();
// log in console
LogTrace($"GetOrSetCacheAsync | {source} | trace: {activity?.TraceId} | {activity?.Duration.TotalMilliseconds:N3}ms", NLog.LogLevel.Trace, caller);
return result!;
}
/// <summary>
/// Helper trace messaggio log (SE abilitato)
/// </summary>
/// <param name="traceMsg"></param>
/// <param name="reqLevel"></param>
/// <param name="methodName"></param>
protected 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}");
}
protected string RedHashMpIO(string keyName)
{
string result = keyName;
@@ -240,11 +359,44 @@ namespace MP.Data.Services
return result;
}
/// <summary>
/// Helper generale per la telemetria e gestione eccezioni
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="name"></param>
/// <param name="body"></param>
/// <param name="parameters"></param>
/// <returns></returns>
protected async Task<T> TraceAsync<T>(string name, Func<Activity?, Task<T>> body, object? parameters = null)
{
using var activity = ActivitySource.StartActivity(name);
try
{
if (parameters != null)
{
activity?.SetTag("params", JsonConvert.SerializeObject(parameters));
}
var result = await body(activity);
activity?.SetStatus(ActivityStatusCode.Ok);
activity?.Stop();
LogTrace($"TraceAsync | trace: {activity?.TraceId} | {activity?.Duration.TotalMilliseconds}ms", methodName: name);
return result;
}
catch (Exception ex)
{
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
//Log.Error(ex, "Errore in {MethodName}", name);
LogTrace($"Errore in {name}", NLog.LogLevel.Error, name);
throw; // Riesponi l'eccezione per il tracking globale
}
}
#endregion Protected Methods
#region Private Fields
private static Logger Log = LogManager.GetCurrentClassLogger();
private bool _disposed = false;
/// <summary>
/// Durata cache lunga IN SECONDI
@@ -258,6 +410,11 @@ namespace MP.Data.Services
private Random rnd = new Random();
/// <summary>
/// Path base chiavi REDIS
/// </summary>
protected readonly string _redisBaseKey = "MP:IOC";
#endregion Private Fields
}
}
+12 -12
View File
@@ -59,9 +59,9 @@ namespace MP.Data.Services
Stopwatch sw = new Stopwatch();
sw.Start();
List<DbSizeModel> result = new List<DbSizeModel>();
// cerco in redisConn...
// cerco in _redisConn...
string currKey = $"{redisBaseKey}:DbInfo:ALL";
RedisValue rawData = redisDb.StringGet(currKey);
RedisValue rawData = _redisDb.StringGet(currKey);
if (rawData.HasValue)
{
result = JsonConvert.DeserializeObject<List<DbSizeModel>>($"{rawData}");
@@ -72,7 +72,7 @@ namespace MP.Data.Services
result = dbController.AllDbInfo();
// serializzo e salvo...
rawData = JsonConvert.SerializeObject(result);
redisDb.StringSet(currKey, rawData, UltraFastCache);
_redisDb.StringSet(currKey, rawData, UltraFastCache);
}
if (result == null)
{
@@ -95,7 +95,7 @@ namespace MP.Data.Services
string currKey = $"{redisBaseKey}:IobList";
Stopwatch sw = new Stopwatch();
sw.Start();
string? rawData = redisDb.StringGet(currKey);
string? rawData = _redisDb.StringGet(currKey);
if (!string.IsNullOrEmpty(rawData) && rawData.Length > 2)
{
source = "REDIS";
@@ -123,9 +123,9 @@ namespace MP.Data.Services
//ordino x idxmacchina...
dbResult = dbResult.OrderBy(x => x.IdxMacchina).ToList();
// serializzo in cache redisConn
// serializzo in cache _redisConn
rawData = JsonConvert.SerializeObject(dbResult, JSSettings);
redisDb.StringSet(currKey, rawData, UltraLongCache);
_redisDb.StringSet(currKey, rawData, UltraLongCache);
}
if (dbResult == null)
{
@@ -147,9 +147,9 @@ namespace MP.Data.Services
Stopwatch sw = new Stopwatch();
sw.Start();
List<RemoteRebootLogModel> result = new List<RemoteRebootLogModel>();
// cerco in redisConn...
// cerco in _redisConn...
string currKey = $"{redisBaseKey}:RemRebLog:ALL";
RedisValue rawData = redisDb.StringGet(currKey);
RedisValue rawData = _redisDb.StringGet(currKey);
//if (!string.IsNullOrEmpty($"{rawData}"))
if (rawData.HasValue)
{
@@ -161,7 +161,7 @@ namespace MP.Data.Services
result = dbController.RemRebootLogGetAll();
// serializzo e salvo...
rawData = JsonConvert.SerializeObject(result);
redisDb.StringSet(currKey, rawData, UltraFastCache);
_redisDb.StringSet(currKey, rawData, UltraFastCache);
}
if (result == null)
{
@@ -183,9 +183,9 @@ namespace MP.Data.Services
Stopwatch sw = new Stopwatch();
sw.Start();
List<RemoteRebootLogModel> result = new List<RemoteRebootLogModel>();
// cerco in redisConn...
// cerco in _redisConn...
string currKey = $"{redisBaseKey}:RemRebLog:LAST";
RedisValue rawData = redisDb.StringGet(currKey);
RedisValue rawData = _redisDb.StringGet(currKey);
//if (!string.IsNullOrEmpty($"{rawData}"))
if (rawData.HasValue)
{
@@ -197,7 +197,7 @@ namespace MP.Data.Services
result = dbController.RemRebootLogGetLast();
// serializzo e salvo...
rawData = JsonConvert.SerializeObject(result);
redisDb.StringSet(currKey, rawData, UltraFastCache);
_redisDb.StringSet(currKey, rawData, UltraFastCache);
}
if (result == null)
{
+22 -22
View File
@@ -63,9 +63,9 @@ namespace MP.Data.Services
Stopwatch sw = new Stopwatch();
sw.Start();
List<AnagArticoliModel>? result = new List<AnagArticoliModel>();
// cerco in redisConn...
// cerco in _redisConn...
string currKey = $"{redisBaseKey}:Art:{azienda}:{searchVal}:{numRecord}";
RedisValue rawData = await redisDb.StringGetAsync(currKey);
RedisValue rawData = await _redisDb.StringGetAsync(currKey);
if (rawData.HasValue)
{
result = JsonConvert.DeserializeObject<List<AnagArticoliModel>>($"{rawData}");
@@ -79,7 +79,7 @@ namespace MP.Data.Services
#endif
// serializzp e salvo...
rawData = JsonConvert.SerializeObject(result);
await redisDb.StringSetAsync(currKey, rawData, LongCache);
await _redisDb.StringSetAsync(currKey, rawData, LongCache);
}
if (result == null)
{
@@ -100,9 +100,9 @@ namespace MP.Data.Services
Stopwatch sw = new Stopwatch();
sw.Start();
List<ConfigModel>? result = new List<ConfigModel>();
// cerco in redisConn...
// cerco in _redisConn...
string currKey = $"{redisBaseKey}:Conf";
RedisValue rawData = await redisDb.StringGetAsync(currKey);
RedisValue rawData = await _redisDb.StringGetAsync(currKey);
if (rawData.HasValue)
{
result = JsonConvert.DeserializeObject<List<ConfigModel>>($"{rawData}");
@@ -113,7 +113,7 @@ namespace MP.Data.Services
result = dbController.ConfigGetAll();
// serializzo e salvo...
rawData = JsonConvert.SerializeObject(result);
redisDb.StringSet(currKey, rawData, LongCache);
_redisDb.StringSet(currKey, rawData, LongCache);
}
if (result == null)
{
@@ -156,9 +156,9 @@ namespace MP.Data.Services
Stopwatch sw = new Stopwatch();
sw.Start();
List<LinkMenuModel>? result = new List<LinkMenuModel>();
// cerco in redisConn...
// cerco in _redisConn...
string currKey = $"{redisBaseKey}:Menu:ALL";
RedisValue rawData = await redisDb.StringGetAsync(currKey);
RedisValue rawData = await _redisDb.StringGetAsync(currKey);
if (rawData.HasValue)
{
result = JsonConvert.DeserializeObject<List<LinkMenuModel>>($"{rawData}");
@@ -169,7 +169,7 @@ namespace MP.Data.Services
result = await Task.FromResult(dbController.ListLinkAll());
// serializzp e salvo...
rawData = JsonConvert.SerializeObject(result);
await redisDb.StringSetAsync(currKey, rawData, UltraLongCache);
await _redisDb.StringSetAsync(currKey, rawData, UltraLongCache);
}
if (result == null)
{
@@ -191,9 +191,9 @@ namespace MP.Data.Services
Stopwatch sw = new Stopwatch();
sw.Start();
List<LinkMenuModel>? result = new List<LinkMenuModel>();
// cerco in redisConn...
// cerco in _redisConn...
string currKey = $"{redisBaseKey}:Menu:{tipoLink}";
RedisValue rawData = await redisDb.StringGetAsync(currKey);
RedisValue rawData = await _redisDb.StringGetAsync(currKey);
//if (!string.IsNullOrEmpty($"{rawData}"))
if (rawData.HasValue)
{
@@ -205,7 +205,7 @@ namespace MP.Data.Services
result = await Task.FromResult(dbController.ListLinkFilt(tipoLink));
// serializzp e salvo...
rawData = JsonConvert.SerializeObject(result);
await redisDb.StringSetAsync(currKey, rawData, UltraLongCache);
await _redisDb.StringSetAsync(currKey, rawData, UltraLongCache);
}
if (result == null)
{
@@ -227,9 +227,9 @@ namespace MP.Data.Services
Stopwatch sw = new Stopwatch();
sw.Start();
List<MacchineModel>? result = new List<MacchineModel>();
// cerco in redisConn...
// cerco in _redisConn...
string currKey = $"{redisBaseKey}:Macc:{MatrOpr}";
RedisValue rawData = await redisDb.StringGetAsync(currKey);
RedisValue rawData = await _redisDb.StringGetAsync(currKey);
//if (!string.IsNullOrEmpty($"{rawData}"))
if (rawData.HasValue)
{
@@ -241,7 +241,7 @@ namespace MP.Data.Services
result = await Task.FromResult(dbController.MacchineByMatrOper(MatrOpr));
// serializzp e salvo...
rawData = JsonConvert.SerializeObject(result);
await redisDb.StringSetAsync(currKey, rawData, UltraLongCache);
await _redisDb.StringSetAsync(currKey, rawData, UltraLongCache);
}
if (result == null)
{
@@ -287,31 +287,31 @@ namespace MP.Data.Services
#region Private Methods
/// <summary>
/// Esegue flush memoria redisConn dato pat2Flush
/// Esegue flush memoria _redisConn dato pat2Flush
/// </summary>
/// <param name="pat2Flush"></param>
/// <returns></returns>
private async Task<bool> ExecFlushRedisPattern(RedisValue pat2Flush)
{
bool answ = false;
var masterEndpoint = redisConn.GetEndPoints()
.Where(ep => redisConn.GetServer(ep).IsConnected && !redisConn.GetServer(ep).IsReplica)
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);
_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 server = _redisConn.GetServer(masterEndpoint);
var keys = server.Keys(database: _redisDb.Database, pattern: pat2Flush, pageSize: 1000);
var deleteTasks = new List<Task>();
foreach (var key in keys)
{
deleteTasks.Add(redisDb.KeyDeleteAsync(key));
deleteTasks.Add(_redisDb.KeyDeleteAsync(key));
if (deleteTasks.Count >= 1000)
{
await Task.WhenAll(deleteTasks);
+6 -7
View File
@@ -1,8 +1,4 @@
#if false
using Blazored.LocalStorage;
using Blazored.SessionStorage;
#endif
using EgwCoreLib.Utils;
using EgwCoreLib.Utils;
using Microsoft.Extensions.Configuration;
using MP.Data.DbModels;
using MP.Data.DTO;
@@ -186,6 +182,9 @@ namespace MP.Data.Services
}
}
public string PageName;
public string PageIcon;
/// <summary>
/// Diritti utente (solo Funzione/Ruolo)
/// </summary>
@@ -688,7 +687,7 @@ namespace MP.Data.Services
protected IConnectionMultiplexer redisConn = null!;
/// <summary>
/// Oggetto DB redisConn da impiegare x chiamate R/W
/// Oggetto DB _redisConn da impiegare x chiamate R/W
/// </summary>
protected IDatabase redisDb = null!;
@@ -745,7 +744,7 @@ namespace MP.Data.Services
}
/// <summary>
/// Recupero HashSet redisConn come Dictionary
/// Recupero HashSet _redisConn come Dictionary
/// </summary>
/// <param name="currKey"></param>
/// <param name="dict"></param>
+6 -6
View File
@@ -60,9 +60,9 @@ namespace MP.Data.Services
Stopwatch sw = new Stopwatch();
sw.Start();
List<ConfigModel>? result = new List<ConfigModel>();
// cerco in redisConn...
// cerco in _redisConn...
string currKey = $"{redisBaseKey}:Conf";
RedisValue rawData = await redisDb.StringGetAsync(currKey);
RedisValue rawData = await _redisDb.StringGetAsync(currKey);
//if (!string.IsNullOrEmpty($"{rawData}"))
if (rawData.HasValue)
{
@@ -74,7 +74,7 @@ namespace MP.Data.Services
result = dbController.ConfigGetAll();
// serializzo e salvo...
rawData = JsonConvert.SerializeObject(result);
redisDb.StringSet(currKey, rawData, LongCache);
_redisDb.StringSet(currKey, rawData, LongCache);
}
if (result == null)
{
@@ -103,9 +103,9 @@ namespace MP.Data.Services
Stopwatch sw = new Stopwatch();
sw.Start();
List<ODLExpModel>? result = new List<ODLExpModel>();
// cerco in redisConn...
// cerco in _redisConn...
string currKey = $"{redisBaseKey}:ODL:{IdxMacchina}:{CodArt}";
RedisValue rawData = await redisDb.StringGetAsync(currKey);
RedisValue rawData = await _redisDb.StringGetAsync(currKey);
//if (!string.IsNullOrEmpty($"{rawData}"))
if (rawData.HasValue)
{
@@ -117,7 +117,7 @@ namespace MP.Data.Services
result = await Task.FromResult(dbController.ListODLFilt(inCorso, CodArt, keyRichPart, Reparto, IdxMacchina, startDate, endDate));
// serializzp e salvo...
rawData = JsonConvert.SerializeObject(result);
await redisDb.StringSetAsync(currKey, rawData, LongCache);
await _redisDb.StringSetAsync(currKey, rawData, LongCache);
}
if (result == null)
{
+2 -2
View File
@@ -68,7 +68,7 @@ namespace MP.Data.Services
//Dictionary<string, List<EventDto>>? result = null;
//// cerco in redis...
//string currKey = $"{redisBaseKey}:PlannerData:{dtStart:yyyyMMdd}:{dtEnd:yyyyMMdd}";
//RedisValue rawData = await redisDb.StringGetAsync(currKey);
//RedisValue rawData = await _redisDb.StringGetAsync(currKey);
////if (!string.IsNullOrEmpty($"{rawData}"))
//if (rawData.HasValue && rawData.Length() > 2)
//{
@@ -80,7 +80,7 @@ namespace MP.Data.Services
// result = dataSimController.PlannerGetEvents(dtStart, dtEnd);
// // serializzo e salvo...
// rawData = JsonConvert.SerializeObject(result);
// await redisDb.StringSetAsync(currKey, rawData, LongCache);
// await _redisDb.StringSetAsync(currKey, rawData, LongCache);
//}
//activity?.SetTag("data.source", source);
//LogTrace($"{source} | trace: {activity?.TraceId} | {activity?.Duration.TotalMilliseconds}ms");
+5 -5
View File
@@ -91,7 +91,7 @@ namespace MP.Data.Services
Stopwatch sw = new Stopwatch();
sw.Start();
List<ConfigModel>? result = new List<ConfigModel>();
// cerco in redisConn...
// cerco in _redisConn...
string currKey = $"{redisBaseKey}:Conf";
RedisValue rawData = await redisDb.StringGetAsync(currKey);
//if (!string.IsNullOrEmpty($"{rawData}"))
@@ -176,7 +176,7 @@ namespace MP.Data.Services
string source = "DB";
sw.Start();
List<MacchineModel>? result = new List<MacchineModel>();
// cerco in redisConn...
// cerco in _redisConn...
string currKey = $"{Constants.redisMacchine}:ALL";
RedisValue rawData = redisDb.StringGet(currKey);
if (rawData.HasValue)
@@ -207,7 +207,7 @@ namespace MP.Data.Services
string source = "DB";
sw.Start();
List<MacchineModel>? result = new List<MacchineModel>();
// cerco in redisConn...
// cerco in _redisConn...
string currKey = $"{Constants.redisMacchine}:{CodGruppo}";
RedisValue rawData = redisDb.StringGet(currKey);
if (rawData.HasValue)
@@ -326,7 +326,7 @@ namespace MP.Data.Services
string source = "DB";
sw.Start();
List<MappaStatoExplModel>? result = new List<MappaStatoExplModel>();
// cerco in redisConn...
// cerco in _redisConn...
RedisValue rawData = redisDb.StringGet(Constants.redisMseKey);
if (rawData.HasValue && !forceDb)
{
@@ -426,7 +426,7 @@ namespace MP.Data.Services
#region Private Methods
/// <summary>
/// Esegue flush memoria redisConn dato pat2Flush
/// Esegue flush memoria _redisConn dato pat2Flush
/// </summary>
/// <param name="pat2Flush"></param>
/// <returns></returns>
File diff suppressed because it is too large Load Diff
+16 -16
View File
@@ -62,9 +62,9 @@ namespace MP.Data.Services
Stopwatch sw = new Stopwatch();
sw.Start();
List<ConfigModel>? result = new List<ConfigModel>();
// cerco in redisConn...
// cerco in _redisConn...
string currKey = $"{redisBaseKey}:Conf";
RedisValue rawData = await redisDb.StringGetAsync(currKey);
RedisValue rawData = await _redisDb.StringGetAsync(currKey);
//if (!string.IsNullOrEmpty($"{rawData}"))
if (rawData.HasValue)
{
@@ -76,7 +76,7 @@ namespace MP.Data.Services
result = dbController.ConfigGetAll();
// serializzo e salvo...
rawData = JsonConvert.SerializeObject(result);
redisDb.StringSet(currKey, rawData, LongCache);
_redisDb.StringSet(currKey, rawData, LongCache);
}
if (result == null)
{
@@ -122,9 +122,9 @@ namespace MP.Data.Services
Stopwatch sw = new Stopwatch();
sw.Start();
List<LingueModel>? result = new List<LingueModel>();
// cerco in redisConn...
// cerco in _redisConn...
string currKey = $"{redisBaseKey}:Lang";
RedisValue rawData = await redisDb.StringGetAsync(currKey);
RedisValue rawData = await _redisDb.StringGetAsync(currKey);
//if (!string.IsNullOrEmpty($"{rawData}"))
if (rawData.HasValue)
{
@@ -136,7 +136,7 @@ namespace MP.Data.Services
result = dbController.LingueGetAll();
// serializzo e salvo...
rawData = JsonConvert.SerializeObject(result);
redisDb.StringSet(currKey, rawData, UltraLongCache);
_redisDb.StringSet(currKey, rawData, UltraLongCache);
}
if (result == null)
{
@@ -174,9 +174,9 @@ namespace MP.Data.Services
Stopwatch sw = new Stopwatch();
sw.Start();
List<VocabolarioModel>? result = new List<VocabolarioModel>();
// cerco in redisConn...
// cerco in _redisConn...
string currKey = $"{redisBaseKey}:Voc";
RedisValue rawData = await redisDb.StringGetAsync(currKey);
RedisValue rawData = await _redisDb.StringGetAsync(currKey);
//if (!string.IsNullOrEmpty($"{rawData}"))
if (rawData.HasValue)
{
@@ -188,7 +188,7 @@ namespace MP.Data.Services
result = dbController.VocabolarioGetAll();
// serializzo e salvo...
rawData = JsonConvert.SerializeObject(result);
redisDb.StringSet(currKey, rawData, UltraLongCache);
_redisDb.StringSet(currKey, rawData, UltraLongCache);
}
if (result == null)
{
@@ -254,31 +254,31 @@ namespace MP.Data.Services
}
/// <summary>
/// Esegue flush memoria redisConn dato pat2Flush
/// Esegue flush memoria _redisConn dato pat2Flush
/// </summary>
/// <param name="pat2Flush"></param>
/// <returns></returns>
private async Task<bool> ExecFlushRedisPattern(RedisValue pat2Flush)
{
bool answ = false;
var masterEndpoint = redisConn.GetEndPoints()
.Where(ep => redisConn.GetServer(ep).IsConnected && !redisConn.GetServer(ep).IsReplica)
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);
_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 server = _redisConn.GetServer(masterEndpoint);
var keys = server.Keys(database: _redisDb.Database, pattern: pat2Flush, pageSize: 1000);
var deleteTasks = new List<Task>();
foreach (var key in keys)
{
deleteTasks.Add(redisDb.KeyDeleteAsync(key));
deleteTasks.Add(_redisDb.KeyDeleteAsync(key));
if (deleteTasks.Count >= 1000)
{
await Task.WhenAll(deleteTasks);
@@ -0,0 +1,45 @@
using EgwCoreLib.Utils;
using MP.Core.DTO;
using MP.Data.DbModels.Utils;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace MP.Data.Services.Utils
{
public interface IStatsAggrService
{
#region Public Methods
/// <summary>
/// Recupera l'elenco delle statistiche aggregate per un periodo specificato.
/// Utilizza la cache automaticamente.
/// </summary>
/// <param name="dtStart">Data inizio periodo</param>
/// <param name="dtEnd">Data fine periodo</param>
Task<List<StatsAggregatedModel>> GetFiltAsync(DateTime dtStart, DateTime dtEnd);
/// <summary>
/// Restituisce un dizionario di elaborazioni di tipo Pareto su orizzonte settimanale
/// Ogni elaborazione contiene Dictionary in forma pareto per una data statistica
/// </summary>
/// <returns></returns>
Task<Dictionary<string, List<StatDataDTO>>> GetParetoStatsWeekAsync();
/// <summary>
/// Recupera il range di periodi valido per le chiamate aggregate.
/// Utilizza la cache automaticamente.
/// </summary>
Task<DtUtils.Periodo> GetRangeAsync();
/// <summary>
/// Inserisce o aggiorna in batch le statistiche aggregate nel database.
/// Opzionalmente elimina i record precedenti nel periodo specificato.
/// </summary>
/// <param name="listRecords">Elenco dei record da inserire/aggiornare</param>
/// <param name="removeOld">Se true elimina preventivamente i record nel periodo richiesto</param>
Task<int> UpsertManyAsync(List<StatsAggregatedModel> listRecords, bool removeOld);
#endregion Public Methods
}
}
@@ -0,0 +1,68 @@
using EgwCoreLib.Utils;
using MP.Core.DTO;
using MP.Data.DbModels.Utils;
using MP.Data.DTO;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace MP.Data.Services.Utils
{
public interface IStatsDetailService
{
#region Public Methods
/// <summary>
/// Recupera le statistiche di dettaglio per un periodo specificato, con filtri opzionali su ambiente e tipo.
/// Utilizza la cache automaticamente.
/// </summary>
/// <param name="dtStart">Data inizio periodo</param>
/// <param name="dtEnd">Data fine periodo</param>
/// <param name="sDest">Filtro opzionale per destinazione (default: vuoto = tutti)</param>
/// <param name="sType">Filtro opzionale per tipo (default: vuoto = tutti)</param>
Task<List<StatsDetailModel>> GetFiltAsync(DateTime dtStart, DateTime dtEnd, string sDest = "", string sType = "");
/// <summary>
/// Restituisce un Pareto degli eventi (in forma originale) più frequenti dato periodo ed eventuale filtro Destinazione/Tipo
/// </summary>
/// <param name="dtStart">Data inizio periodo</param>
/// <param name="dtEnd">Data fine periodo</param>
/// <param name="maxRec"></param>
/// <param name="sDest">Filtro opzionale per destinazione (default: vuoto = tutti)</param>
/// <param name="sType">Filtro opzionale per tipo (default: vuoto = tutti)</param>
/// <returns></returns>
Task<List<StatsDetailModel>> GetParetoAsync(DateTime dtStart, DateTime dtEnd, int maxRec, string sDest = "", string sType = "");
/// <summary>
/// Restituisce un dizionario di elaborazioni di tipo Pareto
/// Ogni elaborazione contiene Dictionary in forma pareto per una data statistica
/// </summary>
/// <returns></returns>
Task<Dictionary<string, List<StatDataDTO>>> GetParetoStatsDayAsync();
/// <summary>
/// Recupera il range di periodi valido per le chiamate di dettaglio.
/// Utilizza la cache automaticamente.
/// </summary>
/// <param name="sEnvir">Ambiente filtrato</param>
/// <param name="sType">Tipo filtrato</param>
Task<DtUtils.Periodo> GetRangeAsync(string sEnvir, string sType);
/// <summary>
/// Helper conversione dati dettaglio in statistiche da inviare a ChartJS
/// </summary>
/// <param name="rawData"></param>
/// <returns></returns>
List<ChartSeriesDto> GetTimeSeriesData(List<StatsDetailModel> rawData);
/// <summary>
/// Inserisce o aggiorna in batch le statistiche di dettaglio nel database.
/// Opzionalmente elimina i record precedenti nel periodo specificato.
/// </summary>
/// <param name="listRecords">Elenco dei record da inserire/aggiornare</param>
/// <param name="removeOld">Se true elimina preventivamente i record nel periodo richiesto</param>
Task<int> UpsertManyAsync(List<StatsDetailModel> listRecords, bool removeOld);
#endregion Public Methods
}
}
+135
View File
@@ -0,0 +1,135 @@
using EgwCoreLib.Utils;
using Microsoft.Extensions.Configuration;
using MP.Core.DTO;
using MP.Data.DbModels.Utils;
using MP.Data.Repository.Utils;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace MP.Data.Services.Utils
{
public class StatsAggrService : BaseServ, IStatsAggrService
{
#region Public Constructors
public StatsAggrService(
IConfiguration config,
IConnectionMultiplexer redis,
IStatsAggrRepository repo) : base(config, redis)
{
_className = "StatsAggr";
_repo = repo;
}
#endregion Public Constructors
#region Public Methods
/// <inheritdoc />
public async Task<List<StatsAggregatedModel>> GetFiltAsync(DateTime dtStart, DateTime dtEnd)
{
return await TraceAsync($"{_className}.GetFilt", async (activity) =>
{
return await GetOrSetCacheAsync(
$"{_redisBaseKey}:{_className}:DT:{dtStart:yyyyMMdd}:{dtEnd:yyyyMMdd}",
async () => await _repo.GetFiltAsync(dtStart, dtEnd),
UltraLongCache
);
});
}
/// <inheritdoc />
public async Task<Dictionary<string, List<StatDataDTO>>> GetParetoStatsWeekAsync()
{
return await TraceAsync($"{_className}.GetDailyParetoStats", async (activity) =>
{
return await GetOrSetCacheAsync(
$"{_redisBaseKey}:{_className}:ParetoWeek",
async () => await GetParetoDataAsync(),
LongCache
);
});
}
/// <inheritdoc />
public async Task<DtUtils.Periodo> GetRangeAsync()
{
return await TraceAsync($"{_className}.GetRange", async (activity) =>
{
return await GetOrSetCacheAsync(
$"{_redisBaseKey}:{_className}:Range",
async () => await _repo.GetRangeAsync(),
UltraFastCache
);
});
}
/// <inheritdoc />
public async Task<int> UpsertManyAsync(List<StatsAggregatedModel> listRecords, bool removeOld)
{
return await TraceAsync($"{_className}.UpsertMany", async (activity) =>
{
string operation = "UpsertMany";
var success = await _repo.UpsertManyAsync(listRecords, removeOld);
activity?.SetTag("db.operation", operation);
if (success > 0)
{
await ClearCacheAsync($"{_redisBaseKey}:{_className}:*");
}
return success;
});
}
#endregion Public Methods
#region Protected Methods
/// <summary>
/// metodo locale per recupero e trasformazione dati da includere con processo generare di tracking & cache
/// </summary>
/// <returns></returns>
protected async Task<Dictionary<string, List<StatDataDTO>>> GetParetoDataAsync()
{
Dictionary<string, List<StatDataDTO>> result = new();
DateTime oggi = DateTime.Today;
var rawData = await GetFiltAsync(oggi.AddDays(-7), oggi);
// calcolo le varie statistiche...
var pDestRequest = rawData.GroupBy(x => x.Destination)
.Select(g => new StatDataDTO
{
Label = g.Key,
Value = g.Sum(x => x.RequestCount)
})
.OrderByDescending(x => x.Value)
.ToList();
result.Add("Dest.Request (#)", pDestRequest);
var pDestDuration = rawData.GroupBy(x => x.Destination)
.Select(g => new StatDataDTO
{
Label = g.Key,
Value = g.Sum(x => x.RequestCount * x.AvgDuration)
})
.OrderByDescending(x => x.Value)
.ToList();
result.Add("Dest.Duration (ms)", pDestDuration);
return result;
}
#endregion Protected Methods
#region Private Fields
private readonly string _className;
private readonly IStatsAggrRepository _repo;
#endregion Private Fields
}
}
@@ -0,0 +1,231 @@
using EgwCoreLib.Utils;
using Microsoft.Extensions.Configuration;
using MP.Core.DTO;
using MP.Data.DbModels.Utils;
using MP.Data.DTO;
using MP.Data.Repository.Utils;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace MP.Data.Services.Utils
{
public class StatsDetailService : BaseServ, IStatsDetailService
{
#region Public Constructors
public StatsDetailService(
IConfiguration config,
IConnectionMultiplexer redis,
IStatsDetailRepository repo) : base(config, redis)
{
_className = "StatsDetail";
_repo = repo;
}
#endregion Public Constructors
#region Public Methods
/// <inheritdoc />
public async Task<List<StatsDetailModel>> GetFiltAsync(DateTime dtStart, DateTime dtEnd, string sDest = "", string sType = "")
{
return await TraceAsync($"{_className}.GetFilt", async (activity) =>
{
string cacheKey = $"{_redisBaseKey}:{_className}:DT:{dtStart:yyyyMMdd}:{dtEnd:yyyyMMdd}";
if (!string.IsNullOrEmpty(sDest))
{
cacheKey += $":{sDest}";
}
if (!string.IsNullOrEmpty(sType))
{
cacheKey += $":{sType}";
}
return await GetOrSetCacheAsync(
cacheKey,
async () => await _repo.GetFiltAsync(dtStart, dtEnd, sDest, sType),
LongCache
);
});
}
/// <inheritdoc />
public async Task<List<StatsDetailModel>> GetParetoAsync(DateTime dtStart, DateTime dtEnd, int maxRec, string sDest = "", string sType = "")
{
return await TraceAsync($"{_className}.GetFilt", async (activity) =>
{
string cacheKey = $"{_redisBaseKey}:{_className}:PARETO:{dtStart:yyyyMMdd}:{dtEnd:yyyyMMdd}";
if (!string.IsNullOrEmpty(sDest))
{
cacheKey += $":{sDest}";
}
if (!string.IsNullOrEmpty(sType))
{
cacheKey += $":{sType}";
}
return await GetOrSetCacheAsync(
cacheKey,
async () => await _repo.GetParetoAsync(dtStart, dtEnd, maxRec, sDest, sType),
LongCache
);
});
}
/// <inheritdoc />
public async Task<Dictionary<string, List<StatDataDTO>>> GetParetoStatsDayAsync()
{
return await TraceAsync($"{_className}.GetDailyParetoStats", async (activity) =>
{
return await GetOrSetCacheAsync(
$"{_redisBaseKey}:{_className}:ParetoWeek",
async () => await GetParetoDataAsync(),
LongCache
);
});
}
/// <inheritdoc />
public async Task<DtUtils.Periodo> GetRangeAsync(string sEnvir, string sType)
{
return await TraceAsync($"{_className}.GetRange", async (activity) =>
{
return await GetOrSetCacheAsync(
$"{_redisBaseKey}:{_className}:Range:{sEnvir}:{sType}",
async () => await _repo.GetRangeAsync(sEnvir, sType),
UltraFastCache
);
});
}
/// <inheritdoc />
public List<ChartSeriesDto> GetTimeSeriesData(List<StatsDetailModel> rawData)
{
var series = rawData
.GroupBy(s => new { s.Destination, s.Type }) // Raggruppiamo per la chiave composta
.Select(group => new ChartSeriesDto
{
// Creiamo un nome leggibile per la legenda del grafico
SeriesName = $"{group.Key.Destination}|{group.Key.Type}",
// Per ogni gruppo, creiamo la lista dei punti temporali
DataPoints = group
.OrderBy(p => p.Hour) // Fondamentale: l'asse X deve essere cronologico
.Select(p => new chartJsData.chartJsTSerie
{
x = p.Hour,
y = p.AvgDuration // La metrica richiesta
})
.ToList()
})
.OrderBy(s => s.SeriesName) // Opzionale: ordina le serie alfabeticamente
.ToList();
return series;
}
/// <inheritdoc />
public async Task<int> UpsertManyAsync(List<StatsDetailModel> listRecords, bool removeOld)
{
return await TraceAsync($"{_className}.UpsertMany", async (activity) =>
{
string operation = "UpsertMany";
var success = await _repo.UpsertManyAsync(listRecords, removeOld);
activity?.SetTag("db.operation", operation);
if (success > 0)
{
await ClearCacheAsync($"{_redisBaseKey}:{_className}:*");
}
return success;
});
}
#endregion Public Methods
#region Protected Methods
/// <summary>
/// metodo locale per recupero e trasformazione dati da includere con processo generare di tracking & cache
/// </summary>
/// <returns></returns>
protected async Task<Dictionary<string, List<StatDataDTO>>> GetParetoDataAsync()
{
Dictionary<string, List<StatDataDTO>> result = new();
DateTime adesso = DateTime.Now;
var rawData = await GetFiltAsync(adesso.AddDays(-1), adesso);
// calcolo le varie statistiche...
var pDestRequest = rawData.GroupBy(x => x.Destination)
.Select(g => new StatDataDTO
{
Label = g.Key,
Value = g.Sum(x => x.RequestCount)
})
.OrderByDescending(x => x.Value)
.ToList();
result.Add("Dest.Request (#)", pDestRequest);
var pDestDuration = rawData.GroupBy(x => x.Destination)
.Select(g => new StatDataDTO
{
Label = g.Key,
Value = g.Sum(x => x.RequestCount * x.AvgDuration)
})
.OrderByDescending(x => x.Value)
.ToList();
result.Add("Dest.Duration (ms)", pDestDuration);
var pTypeRequest = rawData.GroupBy(x => x.Type)
.Select(g => new StatDataDTO
{
Label = g.Key,
Value = g.Sum(x => x.RequestCount)
})
.OrderByDescending(x => x.Value)
.ToList();
result.Add("Type.Request (#)", pTypeRequest);
var pTypeDuration = rawData.GroupBy(x => x.Type)
.Select(g => new StatDataDTO
{
Label = g.Key,
Value = g.Sum(x => x.RequestCount * x.AvgDuration)
})
.OrderByDescending(x => x.Value)
.ToList();
result.Add("Type.Duration (ms)", pTypeDuration);
// calcolo le varie statistiche COMPOSTE...
var pDestTypeRequest = rawData.GroupBy(x => new { x.Destination, x.Type })
.Select(g => new StatDataDTO
{
Label = $"{g.Key.Destination}.{g.Key.Type}",
Value = g.Sum(x => x.RequestCount)
})
.OrderByDescending(x => x.Value)
.ToList();
result.Add("DestType.Request (#)", pDestTypeRequest);
var pDestTypeDuration = rawData.GroupBy(x => new { x.Destination, x.Type })
.Select(g => new StatDataDTO
{
Label = $"{g.Key.Destination}.{g.Key.Type}",
Value = g.Sum(x => x.RequestCount * x.AvgDuration)
})
.OrderByDescending(x => x.Value)
.ToList();
result.Add("DestType.Duration (ms)", pDestTypeDuration);
return result;
}
#endregion Protected Methods
#region Private Fields
private readonly string _className;
private readonly IStatsDetailRepository _repo;
#endregion Private Fields
}
}
+29
View File
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
@* <base href="/" /> *@
<link rel="stylesheet" href="lib/bootstrap/css/bootstrap.min.css" />
<link rel="stylesheet" href="lib/font-awesome/css/all.min.css" />
<link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="MP.IOC.styles.css" />
<link rel="icon" type="image/png" href="favicon.png" />
@* <HeadOutlet /> *@
<HeadOutlet @rendermode="@(new InteractiveServerRenderMode(prerender: false))" />
</head>
<body>
@* <Routes /> *@
<Routes @rendermode="@(new InteractiveServerRenderMode(prerender: false))" />
<script src="_framework/blazor.web.js"></script>
<script type="text/javascript" src="lib/bootstrap/js/bootstrap.min.js"></script>
@* <script src="lib/Chart.js/chart.js"></script> *@
<script src="lib/Chart.js/chart.umd.js"></script>
<script src="lib/luxon/luxon.js"></script>
<script src="lib/chartjs-adapter-luxon/chartjs-adapter-luxon.umd.js"></script>
<script src="lib/chartBoot.js"></script>
</body>
</html>
+133
View File
@@ -0,0 +1,133 @@
@using MP.Data
@inject IJSRuntime JSRuntime
<canvas id="@Id"></canvas>
@code {
[Parameter]
public string Id { get; set; } = "MyHist";
[Parameter]
public string Legenda { get; set; } = "Legenda";
[Parameter]
public bool Horizontal { get; set; } = false;
[Parameter]
public List<double> Data { get; set; } = new List<double>();
[Parameter]
public List<string> Labels { get; set; } = new List<string>();
[Parameter]
public double AspRatio { get; set; } = 0;
[Parameter]
public List<string> lineColor { get; set; } = new List<string>();
[Parameter]
public List<string> backColor { get; set; } = new List<string>();
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await renderChart();
}
/// <summary>
/// Inizializzazione rendering componente
///
/// partendo da qui:
/// https://www.williamleme.com/posts/2020/003-chartjs-blazor/
/// https://www.puresourcecode.com/dotnet/blazor/using-chart-js-with-blazor/
/// https://www.tutorialsteacher.com/csharp/csharp-anonymous-type
/// </summary>
/// <returns></returns>
protected async Task renderChart()
{
// creazione di un oggetto anonymous type con tutte le opzioni da passare a chart.js, tipo verticale
var config = new
{
type = "bar",
options = new
{
responsive = true,
scales = new
{
yAxes = new
{
suggestedMin = 0,
display = true,
ticks = new
{
beginAtZero = true,
maxTicksLimit = 10
}
}
},
Animation = false,
AspectRatio = AspRatio == 0 ? "auto" : $"{AspRatio}"
},
data = new
{
datasets = new[] {
new {
data = Data,
borderColor = lineColor,
backgroundColor = backColor,
borderWidth = 1,
label = Legenda
}
},
labels = Labels
}
};
// creazione di un oggetto anonymous type con tutte le opzioni da passare a chart.js, tipo verticale
var configHor = new
{
type = "bar",
options = new
{
indexAxis = "y",
responsive = true,
scales = new
{
yAxes = new
{
suggestedMin = 0,
display = true,
ticks = new
{
beginAtZero = true,
maxTicksLimit = 10
}
}
},
Animation = false,
AspectRatio = AspRatio == 0 ? "auto" : $"{AspRatio}"
},
data = new
{
datasets = new[] {
new {
data = Data,
borderColor = lineColor,
backgroundColor = backColor,
borderWidth = 1,
label = Legenda
}
},
labels = Labels
}
};
await Task.Delay(50);
if (Horizontal)
{
await JSRuntime.InvokeVoidAsync("setup", Id, configHor);
}
else
{
await JSRuntime.InvokeVoidAsync("setup", Id, config);
}
}
}
+63
View File
@@ -0,0 +1,63 @@
@inject IJSRuntime JSRuntime
<canvas id="@Id"></canvas>
@code {
public enum ChartType
{
Pie,
Bar
}
[Parameter]
public string Id { get; set; } = "MyChart";
[Parameter]
public ChartType Type { get; set; }
[Parameter]
public string[]? Data { get; set; }
[Parameter]
public string[]? BackgroundColor { get; set; }
[Parameter]
public string[]? Labels { get; set; }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await InitDefault();
}
protected async Task InitDefault()
{
// Here we create an anonymous type with all the options
// that need to be sent to Chart.js
var config = new
{
type = Type.ToString().ToLower(),
options = new
{
responsive = true,
scales = new
{
yAxes = new
{
suggestedMin = 0
}
}
},
data = new
{
datasets = new[]
{
new { data = Data, backgroundColor = BackgroundColor}
},
labels = Labels
}
};
await Task.Delay(50);
await JSRuntime.InvokeVoidAsync("setup", Id, config);
}
}
+123
View File
@@ -0,0 +1,123 @@
@using MP.Data
@inject IJSRuntime JSRuntime
<canvas id="@Id"></canvas>
@code {
[Parameter]
public string Id { get; set; } = "MyTs";
[Parameter]
public string Title { get; set; } = "Demo Line";
[Parameter]
public List<chartJsData.chartJsTSerie> DataTS { get; set; } = null!;
[Parameter]
public List<string> Labels { get; set; } = new List<string>();
[Parameter]
public List<string> lineColor { get; set; } = new List<string>();
[Parameter]
public List<string> backColor { get; set; } = new List<string>();
[Parameter]
public double AspRatio { get; set; } = 0;
[Parameter]
public bool Stepped { get; set; } = false;
[Parameter]
public string MinValue { get; set; } = "0";
[Parameter]
public string MaxValue { get; set; } = "0";
[Parameter]
public int lTens { get; set; } = 0;
/// <summary>
/// Inizializzazione rendering componente
///
/// partendo da qui:
/// https://www.williamleme.com/posts/2020/003-chartjs-blazor/
/// https://www.puresourcecode.com/dotnet/blazor/using-chart-js-with-blazor/
/// https://www.tutorialsteacher.com/csharp/csharp-anonymous-type
/// </summary>
/// <param name="firstRender"></param>
/// <returns></returns>
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await renderChart();
}
protected override async Task OnParametersSetAsync()
{
// await Task.Delay(100);
await renderChart();
}
/// <summary>
/// Inizializzazione rendering componente
///
/// partendo da qui:
/// https://www.williamleme.com/posts/2020/003-chartjs-blazor/
/// https://www.puresourcecode.com/dotnet/blazor/using-chart-js-with-blazor/
/// https://www.tutorialsteacher.com/csharp/csharp-anonymous-type
/// </summary>
/// <returns></returns>
protected async Task renderChart()
{
// creazione di un oggetto anonymous type con tutte le opzioni da passare a chart.js
var config = new
{
type = "line",
options = new
{
responsive = true,
scales = new
{
yAxes = new
{
type = "linear",
// type = "logarithmic",
display = true,
ticks = new
{
maxTicksLimit = 10
},
suggestedMin = MinValue != MaxValue ? MinValue : "auto",
suggestedMax = MinValue != MaxValue ? MaxValue : "auto"
},
xAxes = new
{
type = "time",
distribution = "linear",
}
},
Animation = false,
AspectRatio = AspRatio == 0 ? "auto" : $"{AspRatio}"
},
data = new
{
labels = Labels,
datasets = new[]
{
new {
data = DataTS,
borderColor = lineColor,
backgroundColor = backColor,
lineTension = lTens,
stepped = Stepped,
label = Title
}
}
}
};
await Task.Delay(50);
await JSRuntime.InvokeVoidAsync("setup", Id, config);
}
}
+130
View File
@@ -0,0 +1,130 @@
@using MP.Data
@inject IJSRuntime JSRuntime
<canvas id="@Id"></canvas>
@code {
[Parameter]
public string Id { get; set; } = "MyTs";
[Parameter]
public List<string> Titles { get; set; } = new List<string>() { "Demo Line" };
[Parameter]
public List<List<chartJsData.chartJsTSerie>> DataTSList { get; set; } = null!;
[Parameter]
public List<string> Labels { get; set; } = new List<string>();
[Parameter]
public List<string> lineColor { get; set; } = new List<string>();
[Parameter]
public List<string> backColor { get; set; } = new List<string>();
[Parameter]
public double AspRatio { get; set; } = 0;
[Parameter]
public bool Stepped { get; set; } = false;
[Parameter]
public bool Stacked { get; set; } = false;
[Parameter]
public string MinValue { get; set; } = "0";
[Parameter]
public string MaxValue { get; set; } = "0";
[Parameter]
public int lTens { get; set; } = 0;
[Parameter]
public string yScale { get; set; } = "linear";
/// <summary>
/// Inizializzazione rendering componente
///
/// partendo da qui:
/// https://www.williamleme.com/posts/2020/003-chartjs-blazor/
/// https://www.puresourcecode.com/dotnet/blazor/using-chart-js-with-blazor/
/// https://www.tutorialsteacher.com/csharp/csharp-anonymous-type
/// </summary>
/// <param name="firstRender"></param>
/// <returns></returns>
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await renderChart();
}
/// <summary>
/// Inizializzazione rendering componente
///
/// partendo da qui:
/// https://www.williamleme.com/posts/2020/003-chartjs-blazor/
/// https://www.puresourcecode.com/dotnet/blazor/using-chart-js-with-blazor/
/// https://www.tutorialsteacher.com/csharp/csharp-anonymous-type
/// </summary>
/// <returns></returns>
protected async Task renderChart()
{
int i = 0;
// preparo lista di dati...
var dataSetsList = DataTSList.Select(dTS => new
{
data = dTS,
borderColor = lineColor[i],
backgroundColor = backColor,
lineTension = lTens,
stepped = Stepped,
label = Titles[i++]
}).ToList();
// creazione di un oggetto anonymous type con tutte le opzioni da passare a chart.js
var config = new
{
type = "line",
options = new
{
responsive = true,
scales = new
{
y = new
{
stacked = Stacked,
type = yScale,
// type = "logarithmic",
display = true,
ticks = new
{
maxTicksLimit = 10
},
suggestedMin = MinValue != MaxValue ? MinValue : "auto",
suggestedMax = MinValue != MaxValue ? MaxValue : "auto"
},
x = new
{
type = "time",
time = new
{
unit = "hour" // Forza l'unità temporale se i dati sono orari
},
distribution = "linear",
}
},
animation = false,
aspectRatio = AspRatio == 0 ? (object)null : AspRatio
},
data = new
{
labels = Labels,
datasets = dataSetsList
}
};
await Task.Delay(50);
await JSRuntime.InvokeVoidAsync("setup", Id, config);
}
}
+98
View File
@@ -0,0 +1,98 @@
@using MP.Data
@inject IJSRuntime JSRuntime
<canvas id="@Id"></canvas>
@code {
[Parameter]
public string Id { get; set; } = "MyTs";
[Parameter]
public string Title { get; set; } = "DEMO Pie Chart";
[Parameter]
public string LegendPos { get; set; } = "right";
[Parameter]
public List<double> Data { get; set; } = new List<double>();
[Parameter]
public List<string> Labels { get; set; } = new List<string>();
[Parameter]
public List<string> lineColor { get; set; } = new List<string>();
[Parameter]
public List<string> backColor { get; set; } = new List<string>();
[Parameter]
public double AspRatio { get; set; } = 0;
/// <summary>
/// Inizializzazione rendering componente
///
/// partendo da qui:
/// https://www.williamleme.com/posts/2020/003-chartjs-blazor/
/// https://www.puresourcecode.com/dotnet/blazor/using-chart-js-with-blazor/
/// https://www.tutorialsteacher.com/csharp/csharp-anonymous-type
/// </summary>
/// <param name="firstRender"></param>
/// <returns></returns>
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await renderChart();
}
/// <summary>
/// Inizializzazione rendering componente
///
/// partendo da qui:
/// https://www.williamleme.com/posts/2020/003-chartjs-blazor/
/// https://www.puresourcecode.com/dotnet/blazor/using-chart-js-with-blazor/
/// https://www.tutorialsteacher.com/csharp/csharp-anonymous-type
/// </summary>
/// <returns></returns>
protected async Task renderChart()
{
// creazione di un oggetto anonymous type con tutte le opzioni da passare a chart.js
var config = new
{
type = "pie",
options = new
{
responsive = true,
plugins = new
{
legend = new
{
position = LegendPos
},
title = new
{
display = true,
text = Title
}
},
Animation = false,
AspectRatio = AspRatio == 0 ? "auto" : $"{AspRatio}",
HoverBorderWidth = 3
},
data = new
{
labels = Labels.ToArray(),
datasets = new[]
{
new
{
data = Data,
borderColor= lineColor,
backgroundColor= backColor
}
}
}
};
await Task.Delay(50);
await JSRuntime.InvokeVoidAsync("setup", Id, config);
}
}
+94
View File
@@ -0,0 +1,94 @@
@using MP.Data
@inject IJSRuntime JSRuntime
<canvas id="@Id"></canvas>
@code {
[Parameter]
public string Id { get; set; } = "MyTs";
[Parameter]
public List<chartJsData.chartJsTSerie> DataTS { get; set; } = null!;
[Parameter]
public List<string> lineColor { get; set; } = new List<string>();
[Parameter]
public List<string> backColor { get; set; } = new List<string>();
[Parameter]
public double AspRatio { get; set; } = 0;
/// <summary>
/// Inizializzazione rendering componente
///
/// partendo da qui:
/// https://www.williamleme.com/posts/2020/003-chartjs-blazor/
/// https://www.puresourcecode.com/dotnet/blazor/using-chart-js-with-blazor/
/// https://www.tutorialsteacher.com/csharp/csharp-anonymous-type
/// </summary>
/// <param name="firstRender"></param>
/// <returns></returns>
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await renderChart();
}
/// <summary>
/// Inizializzazione rendering componente
///
/// partendo da qui:
/// https://www.williamleme.com/posts/2020/003-chartjs-blazor/
/// https://www.puresourcecode.com/dotnet/blazor/using-chart-js-with-blazor/
/// https://www.tutorialsteacher.com/csharp/csharp-anonymous-type
/// </summary>
/// <returns></returns>
protected async Task renderChart()
{
// creazione di un oggetto anonymous type con tutte le opzioni da passare a chart.js
var config = new
{
type = "line",
options = new
{
responsive = true,
scales = new
{
yAxes = new
{
display = true,
ticks = new
{
maxTicksLimit = 10
}
},
xAxes = new
{
type = "timeseries",
distribution = "linear",
}
},
Animation = false,
AspectRatio = AspRatio == 0 ? "auto" : $"{AspRatio}"
},
data = new
{
datasets = new[]
{
new
{
data = DataTS,
borderColor= lineColor,
backgroundColor= backColor,
lineTension= 0,
stepped= true,
label= "Temperatura Rilevata"
}
}
}
};
await Task.Delay(50);
await JSRuntime.InvokeVoidAsync("setup", Id, config);
}
}
+15
View File
@@ -0,0 +1,15 @@
<div class="d-flex justify-content-between text-light w-100">
<div class="px-1 text-start">
MP-IOC <span class="small">v.@version</span>
</div>
<div class="px-1 text-end">
<span class="small">@adesso</span>
<a class="text-light" href="https://www.egalware.com/" target="_blank">Egalware &nbsp;<img class="img-fluid" width="16" src="images/LogoEgw.png" /></a>
</div>
</div>
@code {
protected DateTime adesso = DateTime.Now;
Version version = typeof(Program).Assembly.GetName().Version;
}
+16
View File
@@ -0,0 +1,16 @@

<div class="row pt-2">
<div class="col-7 col-md-6 col-lg-4 col-xl-3">
<i class="fas fa-user-alt"></i> <b>@userName</b>
</div>
<div class="col-12 col-lg-4 col-xl-6 d-none d-lg-block text-center h4 text-truncate">
<span><i class="@PageIcon" aria-hidden="true"></i> @PageName</span>
</div>
<div class="col-5 col-md-6 col-lg-4 col-xl-3 text-end">
@if (ShowSearch)
{
@* <SearchMod></SearchMod> *@
}
</div>
</div>
+69
View File
@@ -0,0 +1,69 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
namespace MP.IOC.Components.Compo
{
public partial class CmpTop : IDisposable
{
#region Public Methods
public void Dispose()
{
GC.Collect();
}
#endregion Public Methods
#region Protected Properties
[Inject]
protected AuthenticationStateProvider AuthenticationStateProvider { get; set; } = null!;
#endregion Protected Properties
#region Protected Methods
protected override async Task OnInitializedAsync()
{
await forceReload();
}
#endregion Protected Methods
#region Private Fields
private string userName = "";
#endregion Private Fields
#region Private Properties
private string PageIcon { get; set; }
private string PageName { get; set; }
[CascadingParameter(Name = "ShowSearch")]
private bool ShowSearch { get; set; }
#endregion Private Properties
#region Private Methods
private async Task forceReload()
{
userName = "N.A.";
await Task.Delay(1); var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
if (user.Identity != null && user.Identity.IsAuthenticated)
{
userName = $"{user.Identity.Name}";
}
else
{
userName = "N.A.";
}
}
#endregion Private Methods
}
}
@@ -0,0 +1,35 @@

<div class="card shadow">
<div class="card-header">
<div class="d-flex justify-content-between">
<div class="px-0 fw-bold">@Title</div>
<div class="px-0">
<button class="btn btn-sm btn-info" @onclick="() => DoReset()">
<i class="fa-solid fa-arrow-rotate-right"></i>
</button>
</div>
</div>
</div>
<div class="card-body p-1">
<ul class="list-group">
@foreach (var itemDet in ListPaged)
{
<li class="list-group-item @CheckSelect(itemDet.Label) d-flex justify-content-between align-items-start small py-1">
<div class="fs-6 align-items-center">
<button class="btn btn-sm btn-info" @onclick="() => DoSelect(itemDet.Label)">
<i class="fa-solid fa-magnifying-glass"></i>
</button>
@itemDet.Label
</div>
<div class="text-end">
<b>@($"{itemDet.Value:N0}")</b>
<div class="small">@($"({valPerc(itemDet.Value):P2})")</div>
</div>
</li>
}
</ul>
</div>
<div class="card-footer py-0 px-1 small">
<EgwCoreLib.Razor.DataPager currPage="@pageNum" PageSize="@numRecPage" totalCount="@totalCount" numPageChanged="SavePage" numRecordChanged="SaveNumRec" DisplSize="DataPager.ObjSize.small"></EgwCoreLib.Razor.DataPager>
</div>
</div>
@@ -0,0 +1,105 @@
using Microsoft.AspNetCore.Components;
using MP.Core.DTO;
namespace MP.IOC.Components.Compo
{
public partial class ParetoDetail
{
#region Public Properties
[Parameter]
public EventCallback<string> EC_Selected { get; set; }
[Parameter]
public List<StatDataDTO> ParetoList { get; set; } = null!;
[Parameter]
public string Title { get; set; } = null!;
#endregion Public Properties
#region Protected Methods
protected override void OnParametersSet()
{
//base.OnParametersSet();
totalCount = ParetoList.Count();
grandTotal = ParetoList.Sum(x => x.Value);
if (grandTotal == 0)
{
grandTotal = 1;
}
UpdateTable();
}
protected double valPerc(double valore)
{
return valore / grandTotal;
}
#endregion Protected Methods
#region Private Fields
private string currSelect = "";
private double grandTotal = 1;
private List<StatDataDTO> ListPaged = new();
private int numRecPage = 10;
private int pageNum = 1;
private int totalCount = 0;
#endregion Private Fields
#region Private Methods
private string CheckSelect(string curKey)
{
return !string.IsNullOrEmpty(currSelect) && currSelect == curKey ? "bg-dark text-light bg-gradient" : "";
}
private async Task DoReset()
{
currSelect = "";
await EC_Selected.InvokeAsync("");
}
private async Task DoSelect(string reqKey)
{
if (ParetoList.Any(x => x.Label == reqKey))
{
currSelect = reqKey;
await EC_Selected.InvokeAsync(reqKey);
}
}
private void SaveNumRec(int newNum)
{
numRecPage = newNum;
UpdateTable();
}
private void SavePage(int newNum)
{
pageNum = newNum;
UpdateTable();
}
private void UpdateTable()
{
// esegue paginazione
if (totalCount > numRecPage)
{
ListPaged = ParetoList.Skip((pageNum - 1) * numRecPage).Take(numRecPage).ToList();
}
else
{
ListPaged = ParetoList;
}
}
#endregion Private Methods
}
}
+22
View File
@@ -0,0 +1,22 @@
@inherits LayoutComponentBase
<PageTitle>MP-IOC</PageTitle>
<div class="page">
<div class="@sideClass d-print-none">
<NavMenu EC_compressUpdated="UpdateNavDisplay" />
</div>
<div class="main pe-1">
<div class="top-row d-print-none">
<CmpTop></CmpTop>
</div>
<div class="content pt-1 pt-lg-2 mb-5 px-3">
@Body
</div>
<div class="fixed-bottom bottom-row">
<CmpFooter></CmpFooter>
</div>
</div>
</div>
@@ -0,0 +1,52 @@
namespace MP.IOC.Components.Layout
{
public partial class MainLayout : IDisposable
{
#region Public Methods
public void Dispose()
{
GC.Collect();
}
public void OnHideSearch()
{
ShowSearch = false;
}
public void OnShowSearch()
{
ShowSearch = true;
}
#endregion Public Methods
#region Protected Properties
protected bool annualAuthOk { get; set; } = false;
protected string cssRow { get; set; } = "bottom-row bg-primary";
protected bool navLarge { get; set; } = true;
protected string sideClass { get; set; } = "sidebar";
#endregion Protected Properties
#region Protected Methods
protected void UpdateNavDisplay()
{
navLarge = !navLarge;
sideClass = navLarge ? "sidebar" : "sidebarSmall";
}
#endregion Protected Methods
#region Private Properties
private bool ShowSearch { get; set; } = false;
#endregion Private Properties
}
}
@@ -0,0 +1,68 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
.main {
flex: 1;
}
.sidebar,
.sidebarSmall {
background-image: linear-gradient(180deg, #052767 20%, #3aa6ff 90%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
height: 3.5rem;
align-items: center;
/*justify-content: space-evenly;
display: flex;*/
}
.top-row ::deep a,
.top-row .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
}
.top-row a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
.bottom-row {
color: #dedede;
background-color: #000000;
height: 2rem;
align-items: center;
}
@media (min-width: 992px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.sidebarSmall {
width: 80px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.bottom-row {
position: fixed;
bottom: 0;
z-index: 1;
}
.main > div {
padding-left: 0.5rem !important;
padding-right: 0.5rem !important;
/*padding-left: 2rem !important;
padding-right: 1.5rem !important;*/
}
}
@@ -0,0 +1,80 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
.main {
flex: 1;
}
.sidebar,
.sidebarSmall {
background-image: -webkit-linear-gradient(180deg, rgb(5, 39, 103) 20%, #3aa6ff 90%);
background-image: linear-gradient(180deg, rgb(5, 39, 103) 20%, #3aa6ff 90%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
height: 3.5rem;
align-items: center;
/*justify-content: space-evenly;
display: flex;*/
}
.top-row ::deep a, .top-row .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
}
.top-row a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
.bottom-row {
color: #dedede;
background-color: #000000;
height: 2rem;
align-items: center;
}
@media (min-width: 992px) {
.page {
flex-direction: row;
}
.sidebar {
width: 13.5rem;
height: 100vh;
position: sticky;
top: 0;
}
.sidebarSmall {
width: 4.5rem;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.bottom-row {
position: fixed;
bottom: 0;
z-index: 1;
}
.main > div {
padding-left: 0.5rem !important;
padding-right: 0.5rem !important;
/*padding-left: 2rem !important;
padding-right: 1.5rem !important;*/
}
}
+1
View File
@@ -0,0 +1 @@
.page{position:relative;display:flex;flex-direction:column;}.main{flex:1;}.sidebar,.sidebarSmall{background-image:linear-gradient(180deg,#052767 20%,#3aa6ff 90%);}.top-row{background-color:#f7f7f7;border-bottom:1px solid #d6d5d5;height:3.5rem;align-items:center;}.top-row ::deep a,.top-row .btn-link{white-space:nowrap;margin-left:1.5rem;}.top-row a:first-child{overflow:hidden;text-overflow:ellipsis;}.bottom-row{color:#dedede;background-color:#000;height:2rem;align-items:center;}@media(min-width:992px){.page{flex-direction:row;}.sidebar{width:250px;height:100vh;position:sticky;top:0;}.sidebarSmall{width:80px;height:100vh;position:sticky;top:0;}.top-row{position:sticky;top:0;z-index:1;}.bottom-row{position:fixed;bottom:0;z-index:1;}.main>div{padding-left:.5rem!important;padding-right:.5rem!important;}}
+83
View File
@@ -0,0 +1,83 @@
<div class="top-row ps-2 navbar navbar-dark">
<div class="container-fluid">
@if (!showText)
{
<a class="navbar-brand p-0" @onclick="() => ToggleCompress()">MP <i class="fas fa-caret-square-right"></i></a>
}
else
{
<a class="navbar-brand" @onclick="() => ToggleCompress()">MP.IOC <i class="fas fa-caret-square-left"></i></a>
}
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
<nav class="flex-column">
@if (isLoading)
{
<LoadingData DisplayMode="LoadingData.SpinMode.Growl" DisplaySize="LoadingData.CtrlSize.Small"></LoadingData>
}
else
{
<div class="nav-item px-2">
<NavLink class="nav-link py-0 px-2 mb-0" href="Index" Match="NavLinkMatch.All">
<span class="fas fa-home fs-4 pe-2" aria-hidden="true"></span>
@if (showText)
{
<span class="@hideText">Home</span>
}
</NavLink>
</div>
<div class="nav-item px-2">
<NavLink class="nav-link py-0 px-2 mb-0" href="callstats">
<span class="fas fa-chart-pie fs-4 pe-2" aria-hidden="true"></span>
@if (showText)
{
<span class="@hideText">Call Stats</span>
}
</NavLink>
</div>
<div class="nav-item px-2">
<NavLink class="nav-link py-0 px-2 mb-0" href="RouteConf">
<span class="fas fa-computer fs-4 pe-2" aria-hidden="true"></span>
@if (showText)
{
<span class="@hideText">Route Conf</span>
}
</NavLink>
</div>
@* <div class="nav-item px-2">
<NavLink class="nav-link py-0 px-2 mb-0" href="about">
<span class="fas fa-info-circle fs-4 pe-2" aria-hidden="true"></span>
@if (showText)
{
<span class="@hideText">About</span>
}
</NavLink>
</div>
<div class="nav-item px-2">
<NavLink class="nav-link py-0 px-2 mb-0" href="contacts">
<span class="fas fa-envelope fs-4 pe-2" aria-hidden="true"></span>
@if (showText)
{
<span class="@hideText">Contacts</span>
}
</NavLink>
</div> *@
@* <div class="nav-item px-2">
<NavLink class="nav-link py-0 px-2 mb-0" href="RefreshData">
<span class="fas fa-sync-alt fs-4 pe-2" aria-hidden="true"></span>
@if (showText)
{
<span class="@hideText">Refresh Data</span>
}
</NavLink>
</div> *@
}
</nav>
</div>
+112
View File
@@ -0,0 +1,112 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using MP.AppAuth.Models;
namespace MP.IOC.Components.Layout
{
public partial class NavMenu
{
#region Public Properties
[Parameter]
public EventCallback<bool> EC_compressUpdated { get; set; }
#endregion Public Properties
#region Protected Properties
[Inject]
protected AuthenticationStateProvider AuthStateProvider { get; set; } = null!;
protected string hideText { get => showText ? "" : "invisible"; }
[Inject]
protected NavigationManager NavManager { get; set; } = null!;
protected string pageName
{
get
{
string pName = NavManager.ToBaseRelativePath(NavManager.Uri).ToLower();
if (pName.Contains("?"))
{
pName = pName.Substring(0, pName.IndexOf("?"));
}
return pName;
}
}
protected bool showText { get; set; } = true;
protected List<PermessiModel> UserPerm { get; set; } = new List<PermessiModel>();
protected List<UserDirittiModel> UserRight { get; set; } = new List<UserDirittiModel>();
#endregion Protected Properties
#region Protected Methods
protected override async Task OnInitializedAsync()
{
await ReloadData();
}
protected void ToggleCompress()
{
showText = !showText;
EC_compressUpdated.InvokeAsync(showText);
}
#endregion Protected Methods
#region Private Fields
private bool collapseNavMenu = true;
private bool onlyIcon = false;
private string SafePages = "";
private string userName = "";
#endregion Private Fields
#region Private Properties
[Inject]
private IConfiguration ConfMan { get; set; } = null!;
private bool isLoading { get; set; } = false;
private string NavMenuCssClass => collapseNavMenu ? "collapse" : null;
private string? TextCss => onlyIcon ? "d-none" : "";
#endregion Private Properties
#region Private Methods
private async Task ReloadData()
{
isLoading = true;
// sistemo elenco pagine safe...
SafePages = ConfMan.GetValue<string>("ServerConf:SafePages").ToLower() ?? "";
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
if (user.Identity != null && user.Identity.IsAuthenticated)
{
userName = $"{user.Identity.Name}";
}
else
{
userName = "N.A.";
}
isLoading = false;
await Task.Delay(1);
}
private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
#endregion Private Methods
}
}
@@ -0,0 +1,62 @@
.navbar-toggler {
background-color: rgba(255, 255, 255, 0.1);
}
.top-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.oi {
width: 2rem;
font-size: 1.1rem;
vertical-align: text-top;
top: -2px;
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep a {
color: #d7d7d7;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.25);
color: white;
}
.nav-item ::deep a:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
@media (min-width: 992px) {
.navbar-toggler {
display: none;
}
.collapse {
/* Never collapse the sidebar for wide screens */
display: block;
}
}
+6
View File
@@ -0,0 +1,6 @@
@page "/about"
<h3>About</h3>
@code {
}
+79
View File
@@ -0,0 +1,79 @@
@page "/callstats"
<div class="card shadow">
<div class="card-header">
<h3>Current Call Stats (24h)</h3>
</div>
<div class="card-body p-1">
<div class="row mb-2">
<div class="col-3">
<div class="card shadow">
<div class="card-header">
<div class="d-flex justify-content-between">
<div class="px-0 fw-bold">Avail Data</div>
<div class="px-0">
<button class="btn btn-sm btn-info" @onclick="() => DoReset()">
<i class="fa-solid fa-arrow-rotate-right"></i>
</button>
</div>
</div>
</div>
<div class="card-body p-1">
<ul class="list-group shadow">
@foreach (var item in ParetoDay)
{
<li class="list-group-item @CheckSelect(item.Key) d-flex justify-content-between align-items-start small py-1">
<span class="align-items-center">@item.Key</span>
<button class="btn btn-sm btn-info" @onclick="() => DoSelect(item.Key)">
<i class="fa-solid fa-magnifying-glass"></i>
</button>
</li>
}
</ul>
</div>
</div>
</div>
@if (string.IsNullOrEmpty(currHistId))
{
<div class="col-9">
<div class="alert alert-info">selezionare la metrica</div>
</div>
}
else
{
<div class="col-6">
<BarPlot Id="@currHistId" AspRatio="2" Data="@DatiPareto" Labels="@LabelPareto" Legenda="@currTitle" lineColor="@lineColors" backColor="@bgColors"></BarPlot>
</div>
<div class="col-3">
<PieChart Id="@currPieId" AspRatio="1" LegendPos="none" Data="@DatiPareto" Labels="@LabelPareto" lineColor="@lineColors" backColor="@bgColors" Title="@currTitle"></PieChart>
</div>
}
</div>
@if (!string.IsNullOrEmpty(currHistId))
{
<div class="row mb-2">
<div class="col-3">
<ParetoDetail Title="@currHistId" ParetoList="@currData" EC_Selected="ShowDetail"></ParetoDetail>
</div>
<div class="col-9">
@if (string.IsNullOrEmpty(currDetail))
{
<div class="alert alert-info">&larr; selezionare valore</div>
}
else
{
<b>@currDetail</b>
if (tsDataDetail.Count == 0)
{
<div class="alert alert-info">&larr; No data found</div>
}
else
{
<MultiLine Id="@currTsId" AspRatio="4" DataTSList="@TSDataMulti" Labels="@LabelPlot" lineColor="@lineColorsMLine" backColor="@bgColorsMLine" lTens="0" Titles="@lineTitles" Stepped="false" yScale="logarithmic"></MultiLine>
}
}
</div>
</div>
}
</div>
</div>
+269
View File
@@ -0,0 +1,269 @@
using Microsoft.AspNetCore.Components;
using MP.Core.DTO;
using MP.Data;
using MP.Data.DbModels.Utils;
using MP.Data.DTO;
using MP.Data.Services.Utils;
namespace MP.IOC.Components.Pages
{
public partial class CallStats
{
#region Protected Methods
protected override async Task OnInitializedAsync()
{
await ReloadData();
}
#endregion Protected Methods
#region Private Fields
private List<StatDataDTO> currData = new();
private string currDetail = "";
private string currHistId = "";
private string currId = "";
private string currPieId = "";
private string currTitle = "";
private string currTsId = "";
private List<string> lineTitles = new List<string>();
private Dictionary<string, List<StatDataDTO>> ParetoDay = new();
private List<chartJsData.chartJsTSerie> tsData = new List<chartJsData.chartJsTSerie>();
private List<ChartSeriesDto> tsDataDetail = new();
private List<List<chartJsData.chartJsTSerie>> TSDataMulti = new();
#endregion Private Fields
#region Private Properties
/// <summary>
/// Genera colori sfondo 33% rosso / arancione / giallo
/// </summary>
/// <returns></returns>
private List<string> bgColors
{
get => GetSemaforicColors(currData.Count, "0.3");
}
private List<string> bgColorsMLine
{
get => GetDistinctColors(tsDataDetail.Count, "0.3");
}
private List<double> DatiPareto
{
get => currData.Select(x => x.Value).ToList();
}
private List<string> LabelPareto
{
get => currData.Select(x => x.Label).ToList();
}
private List<string> LabelPlot
{
get => tsData.Select(r => $"{r.x:yyyy-MM-dd}").ToList();
}
/// <summary>
/// Genera colori sfondo 33% rosso / arancione / giallo
/// </summary>
/// <returns></returns>
private List<string> lineColors
{
get => GetSemaforicColors(currData.Count, "1");
}
private List<string> lineColorsMLine
{
get => GetDistinctColors(tsDataDetail.Count, "1");
}
[Inject]
private IStatsDetailService SDetService { get; set; } = null!;
#endregion Private Properties
#region Private Methods
private string CheckSelect(string curKey)
{
return !string.IsNullOrEmpty(currId) && currId == curKey ? "active" : "";
}
private void DoReset()
{
currId = "";
currHistId = "";
currPieId = "";
currTsId = "";
currData = new();
}
private void DoSelect(string reqKey)
{
if (ParetoDay.ContainsKey(reqKey))
{
currId = reqKey;
currHistId = $"Bar_{reqKey}";
currPieId = $"Pie_{reqKey}";
currTsId = $"TS_{reqKey}";
currTitle = $"Pareto | {reqKey}";
currData = ParetoDay[reqKey];
}
}
private async Task ReloadData()
{
ParetoDay = await SDetService.GetParetoStatsDayAsync();
}
/// <summary>
/// Genera colori sfondo 33% rosso / arancione / giallo
/// </summary>
/// <param name="numRecords"></param>
/// <param name="alpha"></param>
/// <returns></returns>
private List<string> semaphColors(int numRecords, string alpha)
{
List<string> answ = new List<string>();
// verde...
for (int i = 0; i < numRecords / 3; i++)
{
answ.Add($"rgba(54, 235, 82, {alpha})");
}
// arancione
for (int i = 0; i < numRecords / 3; i++)
{
answ.Add($"rgba(255, 206, 86, {alpha})");
}
while (answ.Count < numRecords)
{
answ.Add($"rgba(255, 99, 132, {alpha}");
}
return answ;
}
private List<string> GetSemaforicColors(int numRecords, string alpha)
{
List<string> colors = new List<string>();
if (numRecords <= 0) return colors;
if (numRecords == 1) { colors.Add($"rgba(54, 235, 82, {alpha})"); return colors; }
// Definiamo i punti chiave (R, G, B)
(int r, int g, int b) green = (54, 235, 82);
(int r, int g, int b) yellow = (255, 206, 86);
(int r, int g, int b) red = (255, 99, 132);
for (int i = 0; i < numRecords; i++)
{
// t va da 0.0 (primo record) a 1.0 (ultimo record)
double t = (double)i / (numRecords - 1);
int r, g, b;
if (t < 0.5)
{
// Da Verde a Giallo (mappiamo 0->0.5 su 0->1)
double localT = t * 2;
r = (int)(green.r + (yellow.r - green.r) * localT);
g = (int)(green.g + (yellow.g - green.g) * localT);
b = (int)(green.b + (yellow.b - green.b) * localT);
}
else
{
// Da Giallo a Rosso (mappiamo 0.5->1 su 0->1)
double localT = (t - 0.5) * 2;
r = (int)(yellow.r + (red.r - yellow.r) * localT);
g = (int)(yellow.g + (red.g - yellow.g) * localT);
b = (int)(yellow.b + (red.b - yellow.b) * localT);
}
colors.Add($"rgba({r}, {g}, {b}, {alpha})");
}
return colors;
}
//private List<string> GetDistinctColors(int numRecords, string alpha)
//{
// List<string> colors = new List<string>();
// for (int i = 0; i < numRecords; i++)
// {
// // Distribuiamo la tonalità (Hue) uniformemente sui 360 gradi
// double hue = (double)i * 360 / numRecords;
// // Usiamo il formato CSS hsla() che è più semplice da generare direttamente
// // Saturazione 70% e Luminosità 50-60% di solito danno colori vivaci e distinti
// colors.Add($"hsla({hue:0.##}, 70%, 50%, {alpha})");
// }
// return colors;
//}
private List<string> GetDistinctColors(int numRecords, string alpha)
{
List<string> colors = new List<string>();
if (numRecords <= 0) return colors;
// Partiamo dal Blu (240°) invece che dal Rosso (0°)
double startHue = 240.0;
for (int i = 0; i < numRecords; i++)
{
// Distribuiamo la tonalità aggiungendo l'offset iniziale
// L'operatore % 360 assicura di rimanere nel cerchio se superiamo il rosso
double hue = (startHue + ((double)i * 360 / numRecords)) % 360;
// Strategia di distinzione (attiva sempre, ma più efficace sopra i 5 colori)
// Alterniamo i valori per i record pari/dispari
string saturation = "70%";
string lightness = "50%";
if (numRecords > 5)
{
// Se i colori sono molti, alterniamo luminosità e saturazione
// I record dispari saranno più chiari e saturi, i pari più scuri e tenui
lightness = (i % 2 == 0) ? "45%" : "65%";
saturation = (i % 2 == 0) ? "80%" : "60%";
}
colors.Add($"hsla({hue:0.##}, {saturation}, {lightness}, {alpha})");
}
return colors;
}
/// <summary>
/// abilita visualizzaione grafico dettagli x metodo indicato
/// </summary>
/// <param name="selDetail"></param>
private async Task ShowDetail(string selDetail)
{
currDetail = selDetail;
// recupero dettaglio 7gg...
DateTime adesso = DateTime.Now;
List<StatsDetailModel> rawData = await SDetService.GetFiltAsync(adesso.AddDays(-5), adesso, "", selDetail);
// conversione con grouping
tsData = rawData.Select(r => new chartJsData.chartJsTSerie() { x = r.Hour, y = (double)r.AvgDuration })
.OrderBy(o => o.x)
.ToList();
tsDataDetail = SDetService.GetTimeSeriesData(rawData);
lineTitles = tsDataDetail.Select(x => x.SeriesName).ToList();
TSDataMulti = tsDataDetail.Select(x => x.DataPoints).ToList();
}
#endregion Private Methods
}
}
+7
View File
@@ -0,0 +1,7 @@
@page "/contacts"
<h3>Contacts</h3>
@code {
}
+157
View File
@@ -0,0 +1,157 @@
@page "/"
@page "/Home"
@page "/Index"
<div class="card shadow">
<div class="card-header bg-dark text-light">
<div class="row">
<div class="col-3 p-2">
<img src="images/LogoMapo.png" class="image-fluid" height="64" />
</div>
<div class="col-6 p-2 text-center align-content-center">
<div class="fs-1 fw-bold">MAPO MES</div>
<div class="fs-6 text-secondary">Rest API target</div>
</div>
<div class="col-3 text-end p-2">
<img src="images/LogoEgw.png" class="image-fluid" height="64" />
</div>
</div>
</div>
<div class="card-body">
<div class="row">
<div class="col-3">
<ul class="list-group">
<li class="list-group-item d-flex justify-content-between align-items-start">
<div>
<b>Alive</b>
</div>
<div class="text-success">
<i class="fa-solid fa-circle-check"></i>
</div>
</li>
<li class="list-group-item d-flex justify-content-between align-items-start">
<div>
<b>IOB-PI</b> I/O
</div>
<div class="text-danger">
<i class="fa-solid fa-circle-check"></i>
</div>
</li>
<li class="list-group-item d-flex justify-content-between align-items-start">
<div>
<b>IOB-WIN</b> I/O
</div>
<div class="text-danger">
<i class="fa-solid fa-circle-check"></i>
</div>
</li>
<li class="list-group-item d-flex justify-content-between align-items-start">
<div>
<b>File</b> upload
</div>
<div class="text-danger">
<i class="fa-solid fa-circle-check"></i>
</div>
</li>
</ul>
</div>
<div class="col-6 text-center">
<h1>MP-IOC</h1>
<h4>.net Core 8.0</h4>
</div>
<div class="col-3 text-end">
<ul class="list-group">
<li class="list-group-item d-flex justify-content-between align-items-start">
<div>
<b>Bench</b> test
</div>
<div class="text-warning">
<i class="fa-solid fa-circle-check"></i>
</div>
</li>
<li class="list-group-item d-flex justify-content-between align-items-start">
<div>
<b>Parameters</b> management
</div>
<div class="text-danger">
<i class="fa-solid fa-circle-check"></i>
</div>
</li>
<li class="list-group-item d-flex justify-content-between align-items-start">
<div>
<b>Recipe</b> management
</div>
<div class="text-success">
<i class="fa-solid fa-circle-check"></i>
</div>
</li>
</ul>
</div>
</div>
<div class="row my-2">
<div class="col-3">
<ul class="list-group">
<li class="list-group-item active d-flex justify-content-between align-items-start">
<b>Top Hourly Call</b>
</li>
@foreach (var item in ListParetoCall)
{
<li class="list-group-item d-flex justify-content-between align-items-start small py-1">
<div>
@item.Destination | <b>@item.Type</b>
</div>
<div class="">
<b>@($"{item.RequestCount:N0}")</b>
</div>
</li>
}
</ul>
</div>
<div class="col-6 text-center">
<div class="fs-3">IOC Conversion rate</div>
<div class="row">
@foreach (var item in paretoWeek)
{
<div class="col-6">
<ul class="list-group">
<li class="list-group-item active d-flex justify-content-between align-items-start">
<b>@item.Key</b>
</li>
@foreach (var itemDet in item.Value)
{
<li class="list-group-item d-flex justify-content-between align-items-start small py-1">
<div>
<b>@itemDet.Label</b>
</div>
<div class="">
<b>@($"{itemDet.Value:N0}")</b>
</div>
</li>
}
</ul>
</div>
}
</div>
</div>
<div class="col-3">
<ul class="list-group">
<li class="list-group-item active d-flex justify-content-between align-items-start">
<b>Call Processed</b>
</li>
@foreach (var item in ListGlobalCall)
{
<li class="list-group-item d-flex justify-content-between align-items-start small py-1">
<div>
@($"{item.Hour:yyyy-MM-dd}") | <b>@item.Destination</b>
</div>
<div class="">
<b>@($"{item.RequestCount:N0}")</b>
</div>
</li>
}
</ul>
</div>
</div>
</div>
</div>
+53
View File
@@ -0,0 +1,53 @@
using Microsoft.AspNetCore.Components;
using MP.Core.DTO;
using MP.Data.DbModels.Utils;
using MP.Data.Services.Utils;
namespace MP.IOC.Components.Pages
{
public partial class Index
{
#region Protected Methods
protected override async Task OnInitializedAsync()
{
await ReloadData();
}
#endregion Protected Methods
#region Private Fields
private List<StatsAggregatedModel> ListGlobalCall = new();
private List<StatsDetailModel> ListParetoCall = new();
private Dictionary<string, List<StatDataDTO>> paretoWeek = new();
#endregion Private Fields
#region Private Properties
[Inject]
private IStatsAggrService StatsAggrService { get; set; } = null!;
[Inject]
private IStatsDetailService StatsDetService { get; set; } = null!;
#endregion Private Properties
#region Private Methods
private async Task ReloadData()
{
DateTime oggi = DateTime.Today;
DateTime adesso = DateTime.Now;
var rawData = await StatsAggrService.GetFiltAsync(oggi.AddDays(-5), oggi);
ListGlobalCall = rawData.OrderByDescending(x => x.Hour).ToList();
ListParetoCall = await StatsDetService.GetParetoAsync(adesso.AddHours(-1), adesso, 10);
paretoWeek = await StatsAggrService.GetParetoStatsWeekAsync();
}
#endregion Private Methods
}
}
@@ -0,0 +1,7 @@
@page "/RefreshData"
<h3>RefreshData</h3>
@code {
}
+85
View File
@@ -0,0 +1,85 @@
@page "/RouteConf"
<div class="card shadow">
<div class="card-header">
<div class="d-flex justify-content-between">
<div class="px-0">
<h3>Route Configuration (IO/IOC)</h3>
</div>
<div class="px-0">
<div class="input-group">
<span class="input-group-text"><i class="fa-solid fa-magnifying-glass"></i></span>
<input type="text" class="form-control" placeholder="Ricerca: alt-/" accesskey="/" @bind="SearchVal" />
<button class="btn btn-sm @CssReset" @onclick="ResetSearch"><i class="fa-solid fa-rotate-right"></i></button>
</div>
</div>
</div>
</div>
<div class="card-body p-1">
<table class="table table-sm table-striped">
<thead>
<tr>
<th>
<button class="btn btn-sm btn-primary" title="Reset selezione" @onclick="DoReset"><i class="fa-solid fa-arrow-rotate-right"></i></button>
</th>
<th>Metodo</th>
<th class="text-start">IO %</th>
<th class="text-center">Balance</th>
<th class="text-end">IOC %</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var record in ListPaged)
{
<tr class="@CheckSelect(record)">
<td>
@if (SelRecord == null)
{
<button class="btn btn-sm btn-info" @onclick="() => DoEdit(record)"><i class="fa-solid fa-pencil"></i></button>
}
else
{
if (SelRecord.Method == record.Method)
{
<button class="btn btn-sm btn-success" @onclick="() => DoSave(record)"><i class="fa-solid fa-floppy-disk"></i></button>
}
else
{
<button class="btn btn-sm btn-secondary disabled"><i class="fa-solid fa-pencil"></i></button>
}
}
</td>
<td>@record.Method</td>
<td class="text-start">
<button class="btn btn-sm btn-warning" @onclick="() => SetNewWeight(record, 0)" title="Set 100% IO"><i class="fa-solid fa-scale-unbalanced"></i></button>
@record.OldWeight
</td>
<td class="text-center">
<button class="btn btn-sm btn-warning" @onclick="() => SetNewWeight(record, record.NewWeight - 10)" title="Seto 50%">+10%</button>
<button class="btn btn-sm btn-success" @onclick="() => SetNewWeight(record, 50)" title="Seto 50%"><i class="fa-solid fa-scale-balanced"></i></button>
<button class="btn btn-sm btn-info" @onclick="() => SetNewWeight(record, record.NewWeight + 10)" title="Seto 50%">+10%</button>
</td>
<td class="text-end">
@record.NewWeight
<button class="btn btn-sm btn-info" @onclick="() => SetNewWeight(record, 100)" title="Set 100% IOC"><i class="fa-solid fa-scale-unbalanced-flip"></i></button>
</td>
<td class="text-end">
@if (SelRecord != null && SelRecord.Method == record.Method)
{
<button class="btn btn-sm btn-warning" @onclick="DoReset"><i class="fa-solid fa-ban"></i></button>
}
</td>
</tr>
}
</tbody>
<tfoot>
<tr>
<td colspan="10">
<EgwCoreLib.Razor.DataPager currPage="@pageNum" PageSize="@numRecPage" totalCount="@totalCount" numPageChanged="SavePage" numRecordChanged="SaveNumRec" DisplSize="DataPager.ObjSize.small"></EgwCoreLib.Razor.DataPager>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
+166
View File
@@ -0,0 +1,166 @@
using Microsoft.AspNetCore.Components;
using MP.Core.DTO;
using MP.IOC.Services;
namespace MP.IOC.Components.Pages
{
public partial class RouteConf
{
#region Protected Properties
protected string CssReset
{
get => string.IsNullOrEmpty(SearchVal) ? "btn-outline-secondary" : "btn-primary";
}
protected string SearchVal
{
get => _searchVal;
set
{
if (_searchVal != value)
{
_searchVal = value;
UpdateTable();
}
}
}
#endregion Protected Properties
#region Protected Methods
protected string CheckSelect(WeightDTO currRec)
{
return SelRecord != null && SelRecord.Method == currRec.Method ? "table-info" : "";
}
protected override async Task OnInitializedAsync()
{
//return base.OnInitializedAsync();
await ReloadData();
UpdateTable();
}
protected void ResetSearch()
{
SearchVal = "";
//UpdateTable();
}
#endregion Protected Methods
#region Private Fields
private string _searchVal = "";
private List<WeightDTO> ListComplete = new();
private List<WeightDTO> ListPaged = new();
private List<WeightDTO> ListSearch = new();
private int numRecPage = 10;
private int pageNum = 1;
private WeightDTO? SelRecord = null;
private int totalCount = 0;
#endregion Private Fields
#region Private Properties
[Inject]
private IWeightProvider WService { get; set; } = null!;
#endregion Private Properties
#region Private Methods
/// <summary>
/// Edit record selezionato
/// </summary>
/// <param name="curRec"></param>
private void DoEdit(WeightDTO curRec)
{
SelRecord = curRec;
}
/// <summary>
/// Imposta il peso del metodo "New" come richiesto, con eventuale check sui limiti 0-100
/// </summary>
/// <param name="curRec"></param>
/// <param name="newWeight"></param>
private async Task SetNewWeight(WeightDTO curRec, int newWeight)
{
newWeight = Math.Clamp(newWeight, 0, 100);
curRec.NewWeight = newWeight;
curRec.OldWeight = 100 - newWeight;
// salvo!
WService.UpsertWeight(curRec);
// rileggo!
await ReloadData();
UpdateTable();
}
private void DoReset()
{
SelRecord = null;
}
private void DoSave(WeightDTO updRec)
{
// salvo e resetto...
SelRecord = null;
}
private async Task ReloadData()
{
ListComplete = await WService.GetAllWeightsAsync();
}
private void SaveNumRec(int newNum)
{
numRecPage = newNum;
UpdateTable();
}
private void SavePage(int newNum)
{
pageNum = newNum;
UpdateTable();
}
private void UpdateTable()
{
// effettuo eventuale ricerca + conteggio...
if (string.IsNullOrEmpty(SearchVal))
{
ListSearch = ListComplete;
}
else
{
ListSearch = ListComplete
.Where(x => x.Method.Contains(SearchVal, StringComparison.InvariantCultureIgnoreCase))
.ToList();
}
totalCount = ListSearch.Count();
// esegue paginazione
if (totalCount > numRecPage)
{
ListPaged = ListSearch.Skip((pageNum - 1) * numRecPage).Take(numRecPage).ToList();
}
else
{
ListPaged = ListSearch;
}
}
#endregion Private Methods
}
}
+6
View File
@@ -0,0 +1,6 @@
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>
+16
View File
@@ -0,0 +1,16 @@
@using EgwCoreLib.Razor
@using EgwCoreLib.Utils
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using MP.Data
@using MP.Data.Repository
@using MP.Data.Services
@using MP.IOC.Components
@using MP.IOC.Components.Compo
@using MP.IOC.Components.ChartJS
@using System.Net.Http
@using System.Net.Http.Json
+18 -3
View File
@@ -3,6 +3,7 @@ using MP.Data;
using MP.IOC.Data;
using NLog;
using System.Diagnostics;
using System.Reflection;
namespace MP.IOC.Controllers
{
@@ -29,7 +30,9 @@ namespace MP.IOC.Controllers
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet("CountKeys")]
[HttpGet]
[Route("CountKeys")]
[Route("CNTKEY")]
public string CountKeys(string? id)
{
string answ = "ND";
@@ -74,7 +77,8 @@ namespace MP.IOC.Controllers
answ = "";
try
{
Dictionary<string, string> valori = DService.ResetDatiMacchina(id);
Dictionary<string, string> valori = DService.mDatiMacchine(id);
//Dictionary<string, string> valori = DService.ResetDatiMacchina(id);
foreach (var item in valori)
{
answ += $"{item.Key}|{item.Value}{Environment.NewLine}";
@@ -117,7 +121,7 @@ namespace MP.IOC.Controllers
answ = "";
try
{
var fiHASH = Utils.hSMI(idxFamIn);
var fiHASH = Utils.GetHashSMI(idxFamIn);
string outVal = "";
bool trovato = DService.RedisHashPresent(fiHASH);
if (!trovato)
@@ -356,6 +360,17 @@ namespace MP.IOC.Controllers
return answ;
}
[HttpGet("version")]
public string version()
{
var version = Assembly
.GetExecutingAssembly()
.GetName()
.Version?
.ToString() ?? "unknown";
return version;
}
#endregion Public Methods
#region Protected Properties
File diff suppressed because it is too large Load Diff
+2210 -123
View File
File diff suppressed because it is too large Load Diff
+15 -1
View File
@@ -1,11 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<Version>6.16.2604.1517</Version>
</PropertyGroup>
<ItemGroup>
<Content Remove="compilerconfig.json" />
</ItemGroup>
<ItemGroup>
<_WebToolingArtifacts Remove="Properties\PublishProfiles\IIS01.pubxml" />
<_WebToolingArtifacts Remove="Properties\PublishProfiles\IIS03.pubxml" />
@@ -14,6 +19,7 @@
</ItemGroup>
<ItemGroup>
<None Include="compilerconfig.json" />
<None Include="Properties\PublishProfiles\IIS01.pubxml.user" />
<None Include="Properties\PublishProfiles\IIS03.pubxml.user" />
<None Include="Properties\PublishProfiles\IIS04.pubxml.user" />
@@ -22,9 +28,13 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="EgwCoreLib.Razor" Version="1.5.2511.312" />
<PackageReference Include="EgwCoreLib.Utils" Version="1.5.2511.312" />
<PackageReference Include="Microsoft.AspNetCore.Components" Version="8.0.25" />
<PackageReference Include="NLog" Version="5.3.4" />
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.14" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.9.0" />
<PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" />
</ItemGroup>
<ItemGroup>
@@ -40,6 +50,10 @@
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="Components\Layout\" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="powershell.exe -ExecutionPolicy Unrestricted -NoProfile -NonInteractive -File $(ProjectDir)\post-build.ps1 -ProjectDir $(ProjectDir) -ProjectPath $(ProjectPath)" />
</Target>
+159 -12
View File
@@ -1,40 +1,179 @@
using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.OpenApi.Models;
using MP.Data;
using MP.Data.Repository.Utils;
using MP.Data.Services.Utils;
using MP.IOC.Components;
using MP.IOC.Data;
using MP.IOC.Services;
using NLog;
using NLog.Web;
using StackExchange.Redis;
using System.Net;
using System.Reflection;
var builder = WebApplication.CreateBuilder(args);
// recupero env corrente
var env = builder.Environment;
var logger = LogManager.Setup()
.LoadConfigurationFromAppSettings()
.GetCurrentClassLogger();
logger.Info("Program.cs: startup");
// Add services to the container.
var assemblyVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString();
logger.Info($"MP.IOC | Program.cs: startup | v.{assemblyVersion}");
logger.Info($"Current ASPNETCORE_ENVIRONMENT: {env.EnvironmentName}");
// Config setup
ConfigurationManager configuration = builder.Configuration;
// REDIS setup
logger.Info("Config OK");
string connStringRedis = configuration.GetConnectionString("Redis");
string redisSrvAddr = connStringRedis.Substring(0, connStringRedis.IndexOf(":"));
logger.Info("Setup REDIS OK");
// YARP base config
builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
builder.Services.AddHttpForwarder();
// HttpMessageInvoker (SocketsHttpHandler)
builder.Services.AddSingleton(sp =>
{
var handler = new SocketsHttpHandler
{
AllowAutoRedirect = false,
UseCookies = false,
PooledConnectionLifetime = TimeSpan.FromMinutes(5),
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
};
// Accetta certificati non validi (dev only)
handler.SslOptions = new System.Net.Security.SslClientAuthenticationOptions
{
RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true
};
return new HttpMessageInvoker(handler);
});
logger.Info("YARP reverse proxy configured");
// base services
builder.Services.AddSingleton<PreserveBodyTransformer>();
builder.Services.AddSingleton<RouteStatsManager>();
builder.Services.AddHostedService<MetricsCalcService>();
builder.Services.AddHostedService<MetricsDbFlushService>();
// Registra i servizi per Blazor
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
// Aggiungi Health Checks
//builder.Services.AddHealthChecks()
// .AddSqlServer(builder.Configuration.GetConnectionString("CoreDb"));
// MP.Data DbContext for Stats repositories
string utilsConnString = builder.Configuration.GetConnectionString("MP.Utils") ?? "Server=localhost;Database=MoonPro_Utils; integrated security=True; MultipleActiveResultSets=True; App=MP.IOC;";
builder.Services.AddDbContextFactory<MoonPro_UtilsContext>(options =>
options.UseSqlServer(utilsConnString));
// MP.Data Services Utils - Statistiche DB
builder.Services.AddScoped<IStatsAggrRepository, StatsAggrRepository>();
builder.Services.AddScoped<IStatsDetailRepository, StatsDetailRepository>();
builder.Services.AddScoped<IStatsAggrService, StatsAggrService>();
builder.Services.AddScoped<IStatsDetailService, StatsDetailService>();
// generic controller
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
//builder.Services.AddSwaggerGen();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "EgaWare's Mapo Rest API",
Version = $"v1 | {assemblyVersion}",
Description = $"API documentation for v1 | AssemblyVersion {assemblyVersion}"
});
});
ConfigurationManager configuration = builder.Configuration;
// REDIS setup
logger.Info("Setup REDIS");
string connStringRedis = configuration.GetConnectionString("Redis");
string redisSrvAddr = connStringRedis.Substring(0, connStringRedis.IndexOf(":"));
// avvio oggetto shared x redis...
var redisMultiplexer = ConnectionMultiplexer.Connect(connStringRedis);
builder.Services.AddSingleton<IConnectionMultiplexer>(redisMultiplexer);
var redisMux = ConnectionMultiplexer.Connect(connStringRedis);
builder.Services.AddSingleton<IConnectionMultiplexer>(redisMux);
// oggetto principale accesso dati
builder.Services.AddSingleton<MpDataService>();
logger.Info("Standard service configured");
// WeightProvider: Redis/Memory da config
var weightOnRedis = builder.Configuration.GetValue<bool>("ServerConf:RedisWeight", false);
if (weightOnRedis)
{
builder.Services.AddSingleton<IConnectionMultiplexer>(redisMux);
builder.Services.AddSingleton<IWeightProvider, RedisWeightProvider>();
}
else
{
builder.Services.AddSingleton<IWeightProvider, InMemoryWeightProvider>();
}
logger.Info($"Weight service configured | use Redis: {weightOnRedis}");
// RouteManager registration (singleton)
builder.Services.AddSingleton<RouteManager>();
logger.Info("Singleton Route Manager registered");
var app = builder.Build();
// aggiunt base URL x routing corretto
string baseUrl = configuration.GetValue<string>("ServerConf:BaseUrl") ?? "";
app.UsePathBase(baseUrl);
string routePath = configuration.GetValue<string>("ServerConf:RoutePath") ?? "/api/RIOB";
string fullPath = $"{baseUrl}{routePath}".Replace("//", "/");
logger.Info($"BaseUrl: {baseUrl}");
app.Use(async (ctx, next) =>
{
logger.Debug($"Incoming request PathBase='{ctx.Request.PathBase}' Path='{ctx.Request.Path}' RawTarget='{ctx.Request.GetEncodedUrl()}'");
await next();
});
// Map route to RouteManager
//app.Map("/MP/IOC/api/RIOB/{**catchAll}", async (HttpContext ctx) =>
//{
// var routeManager = ctx.RequestServices.GetRequiredService<RouteManager>();
// await routeManager.HandleAsync(ctx);
//});
// parametrizzare?!?!?
app.MapWhen(ctx =>
ctx.Request.Path.StartsWithSegments(fullPath, StringComparison.OrdinalIgnoreCase) ||
ctx.Request.Path.StartsWithSegments(routePath, StringComparison.OrdinalIgnoreCase) ||
(ctx.Request.PathBase.HasValue && ctx.Request.Path.StartsWithSegments(routePath, StringComparison.OrdinalIgnoreCase)),
builder =>
{
builder.Run(async ctx =>
{
var routeManager = ctx.RequestServices.GetRequiredService<RouteManager>();
await routeManager.HandleAsync(ctx);
});
});
logger.Info("App: route Mapped");
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment() || app.Environment.IsStaging())
{
app.UseSwagger();
app.UseSwaggerUI();
//app.UseSwaggerUI();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint($"{baseUrl}swagger/v1/swagger.json", "EgalWare's MAPO API v1");
});
}
logger.Info("Added swagger");
@@ -45,9 +184,17 @@ app.UseAuthorization();
// aggiunta x index.html
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseAntiforgery();
// Mappatura delle API
app.MapControllers();
// Mappatura della Dashboard Blazor
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
//app.MapHealthChecks("/health");
logger.Info("Run App");
app.Run();
@@ -22,7 +22,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<UserName>jenkins</UserName>
<_SavePWD>true</_SavePWD>
<_TargetId>IISWebDeploy</_TargetId>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
</PropertyGroup>
</Project>
@@ -6,7 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<PropertyGroup>
<TimeStampOfAssociatedLegacyPublishXmlFile />
<EncryptedPassword>AQAAANCMnd8BFdERjHoAwE/Cl+sBAAAAdCoESbxryUqXde3FfEOCTwAAAAACAAAAAAADZgAAwAAAABAAAAAFrH1vByj45Qn06hO/OH6tAAAAAASAAACgAAAAEAAAAIUK5NiEpc4lc11Op6/CLx8YAAAAMecN12fzIN9e3E8R/nu0ATe2PAsMy7M8FAAAAKbUyki2vkSFehjbpB8wCVVVa055</EncryptedPassword>
<History>True|2024-11-04T07:56:17.3071781Z||;True|2023-02-14T17:41:26.3850692+01:00||;True|2023-02-14T17:31:39.4933399+01:00||;</History>
<History>True|2026-04-03T10:55:15.0251473Z||;True|2026-04-03T10:03:02.5833820+02:00||;True|2026-04-03T09:55:55.5274684+02:00||;True|2026-04-03T09:52:44.9063312+02:00||;False|2026-04-03T09:45:49.5943015+02:00||;True|2024-11-04T08:56:17.3071781+01:00||;True|2023-02-14T17:41:26.3850692+01:00||;True|2023-02-14T17:31:39.4933399+01:00||;</History>
<LastFailureDetails />
</PropertyGroup>
</Project>
@@ -22,7 +22,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<UserName>jenkins</UserName>
<_SavePWD>true</_SavePWD>
<_TargetId>IISWebDeploy</_TargetId>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
</PropertyGroup>
</Project>
@@ -22,7 +22,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<UserName>jenkins</UserName>
<_SavePWD>true</_SavePWD>
<_TargetId>IISWebDeploy</_TargetId>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
</PropertyGroup>
</Project>
@@ -15,7 +15,7 @@ by editing this MSBuild file. In order to learn more about this please visit htt
<DesktopBuildPackageLocation>bin\publish\MP.IOC.zip</DesktopBuildPackageLocation>
<PackageAsSingleFile>true</PackageAsSingleFile>
<DeployIisAppPath>Default Web Site/MP/IOC</DeployIisAppPath>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<SelfContained>false</SelfContained>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
</PropertyGroup>
+1 -1
View File
@@ -1,6 +1,6 @@
<body>
<i>Modulo MP-IOC </i>
<h4>Versione: 6.16.2602.2510</h4>
<h4>Versione: 6.16.2604.1517</h4>
<br /> Note di rilascio:
<ul>
<li>
+1 -1
View File
@@ -1 +1 @@
6.16.2602.2510
6.16.2604.1517
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>6.16.2602.2510</version>
<version>6.16.2604.1517</version>
<url>https://nexus.steamware.net/repository/SWS/MP-IOC/stable/LAST/MP.IOC.zip</url>
<changelog>https://nexus.steamware.net/repository/SWS/MP-IOC/stable/LAST/ChangeLog.html</changelog>
<mandatory>false</mandatory>
+372
View File
@@ -0,0 +1,372 @@
using MP.Data;
using Newtonsoft.Json;
using NLog;
using StackExchange.Redis;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace MP.IOC.Services
{
/// <summary>
/// Classe base per i servizi che fornisce funzionalità comuni come
/// - connessione a Redis
/// - configurazione
/// - gestione dei messaggi
/// - strategie di caching.
///
/// Questa classe agisce come modello per altri servizi derivati.
/// </summary>
public class BaseServ
{
#region Public Constructors
/// <summary>
/// Inizializza una nuova istanza della classe BaseServ.
/// Configura la connessione Redis, carica le impostazioni di configurazione e inizializza il serializzatore JSON.
/// </summary>
/// <param name="Configuration">Oggetto di configurazione per recuperare le impostazioni dell'applicazione.</param>
/// <param name="RedisConn">Multiplexer di connessione Redis per operazioni sul database.</param>
public BaseServ(IConfiguration Configuration, IConnectionMultiplexer RedisConn)
{
_config = Configuration;
_redisConn = RedisConn;
_redisDb = _redisConn.GetDatabase();
// configuro la base key x la cache Redis, con verifica contenga Cache finale
_redisBaseKey = _config.GetValue<string>("ServerConf:RedisBaseKey") ?? "Lux:Cache";
// aggiungo cache se non finisse per ":cache"
if (!_redisBaseKey.EndsWith(":Cache"))
{
_redisBaseKey += ":Cache";
}
// Configurazione serializzatore JSON per risolvere errore di loop circolare
JSSettings = new JsonSerializerSettings()
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
};
}
#endregion Public Constructors
#region Public Properties
/// <summary>
/// Pipe dei messaggi per la comunicazione riguardo update calcolo BOM
/// </summary>
public MessagePipe PipeBom { get; set; } = null!;
/// <summary>
/// Pipe dei messaggi per ritorno HwList da Engine di calcolo verso interfaccia utente.
/// I messaggi vengono inviati sul canale Redis definito da ChannelHwList.
/// </summary>
public MessagePipe PipeHwList { get; set; } = null!;
/// <summary>
/// Pipe dei messaggi per ritorno HwOptions calcolate da Engine di calcolo verso interfaccia utente.
/// I messaggi vengono inviati sul canale Redis definito da ChannelHwOpt.
/// </summary>
public MessagePipe PipeHwOpt { get; set; } = null!;
/// <summary>
/// Pipe dei messaggi per ritorno PNG calcolati da Engine di calcolo verso interfaccia utente.
/// I messaggi vengono inviati sul canale Redis definito da ChannelPng.
/// </summary>
public MessagePipe PipePng { get; set; } = null!;
/// <summary>
/// Canale informazioni relativo ad attività relative alla gesitone PROD:
/// - carico macchine
/// - scheduling
/// </summary>
public MessagePipe PipeProd { get; set; } = null!;
/// <summary>
/// Pipe dei messaggi per ritorno info elementi del profile
/// </summary>
public MessagePipe PipeProfElement { get; set; } = null!;
/// <summary>
/// Pipe dei messaggi per ritorno ProfileListAsync calcolate da Engine di calcolo verso interfaccia utente.
/// I messaggi vengono inviati sul canale Redis definito da ChannelProfList.
/// </summary>
public MessagePipe PipeProfList { get; set; } = null!;
/// <summary>
/// Pipe dei messaggi per ritorno Shape calcolate da Engine di calcolo verso interfaccia utente.
/// I messaggi vengono inviati sul canale Redis definito da ChannelShape.
/// </summary>
public MessagePipe PipeShape { get; set; } = null!;
/// <summary>
/// Pipe dei messaggi per ritorno SVG calcolati da Engine di calcolo verso interfaccia utente.
/// I messaggi vengono inviati sul canale Redis definito da ChannelSvg.
/// </summary>
public MessagePipe PipeSvg { get; set; } = null!;
/// <summary>
/// Pipe dei messaggi per la comunicazione riguardo update generico UI
/// </summary>
public MessagePipe PipeUpdate { get; set; } = null!;
#endregion Public Properties
#region Protected Fields
/// <summary>
/// Oggetto per collezione dati Activity (span in Uptrace)
/// </summary>
protected static readonly ActivitySource ActivitySource = new ActivitySource("Lux.DATA");
/// <summary>
/// Oggetto logger utilizzato per registrare eventi e errori a livello di classe.
/// Utile per il monitoraggio del comportamento dell'applicazione e la risoluzione di problemi.
/// </summary>
protected static Logger Log = LogManager.GetCurrentClassLogger();
/// <summary>
/// Oggetto di configurazione statico per accedere alle impostazioni dell'applicazione (es. stringhe di connessione).
/// Condiviso tra tutte le istanze di BaseServ.
/// </summary>
protected readonly IConfiguration _config = null!;
/// <summary>
/// Path base chiavi REDIS
/// </summary>
protected readonly string _redisBaseKey = "Lux:Cache";
/// <summary>
/// Oggetto per la connessione a Redis utilizzato per operazioni di lettura/scrittura.
/// </summary>
protected readonly IConnectionMultiplexer _redisConn = null!;
/// <summary>
/// Database Redis utilizzato per le operazioni di lettura/scrittura
/// nb: ottenuto tramite _redisConn.GetDatabase()
/// </summary>
protected readonly IDatabase _redisDb = null!;
/// <summary>
/// Abilitazione operazioni tracing generiche
/// </summary>
protected readonly bool _traceEnabled = false;
/// <summary>
/// Impostazioni del serializzatore JSON utilizzato per gestire oggetti con riferimenti circolari
/// (es. oggetti che si fanno riferimento reciprocamente).
/// </summary>
protected JsonSerializerSettings? JSSettings;
#endregion Protected Fields
#region Protected Properties
/// <summary>
/// Durata della cache breve (circa 1 minuto + variazione del +/-10%)
/// </summary>
protected TimeSpan FastCache
{
get => TimeSpan.FromSeconds(cacheTtlShort * rnd.Next(900, 1100) / 1000);
}
/// <summary>
/// Durata della cache lunga (+ variazione del +/-10%)
/// </summary>
protected TimeSpan LongCache
{
get => TimeSpan.FromSeconds(cacheTtlLong * rnd.Next(900, 1100) / 1000);
}
/// <summary>
/// Durata della cache molto breve (circa 10 secondi + variazione del +/-10%)
/// </summary>
protected TimeSpan UltraFastCache
{
get => TimeSpan.FromSeconds(cacheTtlShort / 6 * rnd.Next(900, 1100) / 1000);
}
/// <summary>
/// Durata della cache molto lunga (+ variazione del +/-10%)
/// </summary>
protected TimeSpan UltraLongCache
{
get => TimeSpan.FromSeconds(cacheTtlLong * 10 * rnd.Next(900, 1100) / 1000);
}
#endregion Protected Properties
#region Protected Methods
/// <summary>
/// Helper avvio attività per la funzione tracciata
/// </summary>
/// <param name="methodName"></param>
/// <returns></returns>
protected static Activity? StartActivity([CallerMemberName] string? methodName = null)
{
var activity = ActivitySource.StartActivity(methodName ?? "UNDEF");
activity?.SetTag("host.name", Environment.MachineName);
return activity;
}
/// <summary>
/// Invalida una o più chiavi/pattern in Redis
/// </summary>
protected async Task ClearCacheAsync(params string[] patterns)
{
foreach (var pattern in patterns)
{
// Chiamata al tuo metodo esistente
await ExecFlushRedisPatternAsync((RedisValue)pattern);
}
}
/// <summary>
/// Metodo di flush dati cache Redis
/// </summary>
/// <param name="pattern"></param>
/// <returns></returns>
protected async Task ExecFlushRedisPatternAsync(RedisValue pattern)
{
// Qui inserisci la tua logica attuale (es. via Lua script o Keys/Scan)
// Esempio rapido via server scan:
var endpoints = _redisConn.GetEndPoints();
foreach (var endpoint in endpoints)
{
var server = _redisConn.GetServer(endpoint);
await foreach (var key in server.KeysAsync(_redisDb.Database, pattern))
{
await _redisDb.KeyDeleteAsync(key);
}
}
}
/// <summary>
/// Helper generale di lettura da cache o da funzione (DB) con caching successivo
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key"></param>
/// <param name="factory"></param>
/// <param name="expiration"></param>
/// <returns></returns>
protected async Task<T> GetOrSetCacheAsync<T>(string key, Func<Task<T>> factory, TimeSpan? expiration = null, [CallerMemberName] string? caller = null)
{
using var activity = StartActivity();
string source = "DB";
// 1. Provo Redis
var cached = await _redisDb.StringGetAsync(key);
if (cached.HasValue)
{
source = "REDIS";
var cachedResult = JsonConvert.DeserializeObject<T>(cached!)!;
activity?.SetTag("data.source", source);
LogTrace($"{source} | trace: {activity?.TraceId} | {activity?.Duration.TotalMilliseconds}ms", NLog.LogLevel.Trace, caller);
return cachedResult;
}
// 2. Chiamo il factory (DB)
T result = await factory();
if (result != null)
{
// 3. Salva in Redis per la prossima volta
var serialized = JsonConvert.SerializeObject(result, JSSettings);
await _redisDb.StringSetAsync(key, serialized, expiration ?? LongCache);
}
// sistemo activity tracking data
activity?.SetTag("data.source", source);
activity?.Stop();
// log in console
LogTrace($"GetOrSetCacheAsync | {source} | trace: {activity?.TraceId} | {activity?.Duration.TotalMilliseconds:N3}ms", NLog.LogLevel.Trace, caller);
return result!;
}
/// <summary>
/// Helper trace messaggio log (SE abilitato)
/// </summary>
/// <param name="traceMsg"></param>
/// <param name="reqLevel"></param>
/// <param name="methodName"></param>
protected 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}");
}
/// <summary>
/// Helper generale per la telemetria e gestione eccezioni
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="name"></param>
/// <param name="body"></param>
/// <param name="parameters"></param>
/// <returns></returns>
protected async Task<T> TraceAsync<T>(string name, Func<Activity?, Task<T>> body, object? parameters = null)
{
using var activity = ActivitySource.StartActivity(name);
try
{
if (parameters != null)
{
activity?.SetTag("params", JsonConvert.SerializeObject(parameters));
}
var result = await body(activity);
activity?.SetStatus(ActivityStatusCode.Ok);
activity?.Stop();
LogTrace($"TraceAsync | trace: {activity?.TraceId} | {activity?.Duration.TotalMilliseconds}ms", methodName: name);
return result;
}
catch (Exception ex)
{
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
//Log.Error(ex, "Errore in {MethodName}", name);
LogTrace($"Errore in {name}", NLog.LogLevel.Error, name);
throw; // Riesponi l'eccezione per il tracking globale
}
}
#endregion Protected Methods
#region Private Fields
private static readonly ConcurrentDictionary<string, SemaphoreSlim> _locks = new();
/// <summary>
/// Durata della cache lunga in secondi (predefinito: 5 minuti)
/// Utilizzato nella proprietà LongCache per definire quanto a lungo i dati devono essere memorizzati in cache.
/// </summary>
private int cacheTtlLong = 60 * 5;
/// <summary>
/// Durata della cache breve in secondi (predefinito: 1 minuto)
/// Utilizzato nelle proprietà FastCache e UltraFastCache per definire la durata della cache breve.
/// </summary>
private int cacheTtlShort = 60 * 1;
/// <summary>
/// Generatore di numeri casuali utilizzato per introdurre variabilità dinamica nelle durate della cache
/// (simula variazioni reali nella freschezza dei dati e nei tempi di scadenza).
/// </summary>
private Random rnd = new Random();
#endregion Private Fields
#region Private Methods
#endregion Private Methods
}
}

Some files were not shown because too many files have changed in this diff Show More