diff --git a/MP.Data/Repository/Utils/StatsCodeRepository.cs b/MP.Data/Repository/Utils/StatsCodeRepository.cs new file mode 100644 index 00000000..af6871d3 --- /dev/null +++ b/MP.Data/Repository/Utils/StatsCodeRepository.cs @@ -0,0 +1,141 @@ +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 StatsCodeRepository : BaseRepository, IStatsCodeRepository + { + #region Public Constructors + + public StatsCodeRepository(IDbContextFactory ctxFactory) : base(ctxFactory) + { + } + + #endregion Public Constructors + + #region Public Methods + + /// + public async Task> GetFiltAsync(DateTime dtStart, DateTime dtEnd) + { + await using var dbCtx = await CreateContextAsync(); + return await dbCtx + .DbSetStatusCode + .Where(x => x.Hour >= dtStart && x.Hour <= dtEnd) + .AsNoTracking() + .OrderBy(x => x.Hour) + .ToListAsync(); + } + + /// + public async Task GetRangeAsync() + { + await using var dbCtx = await CreateContextAsync(); + DtUtils.Periodo answ = new DtUtils.Periodo(DtUtils.PeriodSet.Today); + var query = dbCtx.DbSetStatusCode.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; + } + + /// + public async Task UpsertManyAsync(List 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 + .DbSetStatusCode + .Where(x => x.Hour >= startDate && x.Hour <= endDate) + .ExecuteDeleteAsync(); +#endif + + var itemsToRemove = await dbCtx.DbSetStatusCode + .Where(x => x.Hour >= minHour && x.Hour <= maxHour) + .ToListAsync(); + if (itemsToRemove.Any()) + { + dbCtx.DbSetStatusCode.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.DbSetStatusCode + .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.Type, x.Hour, x.StatusCode), + x => x + ); + + foreach (var incoming in listRecords) + { + var key = (incoming.Destination, incoming.Type, incoming.Hour, incoming.StatusCode); + if (lookup.TryGetValue(key, out var existing)) + { + // --- CASO: UPDATE --- + existing.Count = incoming.Count; + } + else + { + // --- CASO: INSERT --- + await dbCtx.DbSetStatusCode.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; + } + } + + #endregion Public Methods + + #region Protected Fields + + protected static NLog.Logger Log = LogManager.GetCurrentClassLogger(); + + #endregion Protected Fields + } +} \ No newline at end of file diff --git a/MP.Data/Repository/Utils/StatsErrRepository.cs b/MP.Data/Repository/Utils/StatsErrRepository.cs new file mode 100644 index 00000000..c916d9de --- /dev/null +++ b/MP.Data/Repository/Utils/StatsErrRepository.cs @@ -0,0 +1,141 @@ +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 StatsErrRepository : BaseRepository, IStatsErrRepository + { + #region Public Constructors + + public StatsErrRepository(IDbContextFactory ctxFactory) : base(ctxFactory) + { + } + + #endregion Public Constructors + + #region Public Methods + + /// + public async Task> GetFiltAsync(DateTime dtStart, DateTime dtEnd) + { + await using var dbCtx = await CreateContextAsync(); + return await dbCtx + .DbSetStatsError + .Where(x => x.Hour >= dtStart && x.Hour <= dtEnd) + .AsNoTracking() + .OrderBy(x => x.Hour) + .ToListAsync(); + } + + /// + public async Task GetRangeAsync() + { + await using var dbCtx = await CreateContextAsync(); + DtUtils.Periodo answ = new DtUtils.Periodo(DtUtils.PeriodSet.Today); + var query = dbCtx.DbSetStatsError.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; + } + + /// + public async Task UpsertManyAsync(List 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 + .DbSetStatsError + .Where(x => x.Hour >= startDate && x.Hour <= endDate) + .ExecuteDeleteAsync(); +#endif + + var itemsToRemove = await dbCtx.DbSetStatsError + .Where(x => x.Hour >= minHour && x.Hour <= maxHour) + .ToListAsync(); + if (itemsToRemove.Any()) + { + dbCtx.DbSetStatsError.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.DbSetStatsError + .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.Type, x.Hour, x.ErrorMessage), + x => x + ); + + foreach (var incoming in listRecords) + { + var key = (incoming.Destination, incoming.Type, incoming.Hour, incoming.ErrorMessage); + if (lookup.TryGetValue(key, out var existing)) + { + // --- CASO: UPDATE --- + existing.Count = incoming.Count; + } + else + { + // --- CASO: INSERT --- + await dbCtx.DbSetStatsError.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; + } + } + + #endregion Public Methods + + #region Protected Fields + + protected static NLog.Logger Log = LogManager.GetCurrentClassLogger(); + + #endregion Protected Fields + } +} \ No newline at end of file diff --git a/MP.Data/Services/Utils/IStatsAggrService.cs b/MP.Data/Services/Utils/IStatsAggrService.cs index a1905070..6723a860 100644 --- a/MP.Data/Services/Utils/IStatsAggrService.cs +++ b/MP.Data/Services/Utils/IStatsAggrService.cs @@ -20,13 +20,6 @@ namespace MP.Data.Services.Utils /// Data fine periodo Task> GetFiltAsync(DateTime dtStart, DateTime dtEnd); - /// - /// Restituisce un dizionario di elaborazioni di tipo Pareto su orizzonte settimanale - /// Ogni elaborazione contiene Dictionary in forma pareto per una data statistica - /// - /// - Task>> GetParetoStatsWeekAsync(); - /// /// Restituisce un dizionario di elaborazioni di tipo Pareto su orizzonte giornaliero /// Ogni elaborazione contiene Dictionary in forma pareto per una data statistica @@ -34,19 +27,19 @@ namespace MP.Data.Services.Utils /// Task>> GetParetoStatsDayAsync(int numDay); + /// + /// Restituisce un dizionario di elaborazioni di tipo Pareto su orizzonte settimanale + /// Ogni elaborazione contiene Dictionary in forma pareto per una data statistica + /// + /// + Task>> GetParetoStatsWeekAsync(); + /// /// Recupera il range di periodi valido per le chiamate aggregate. /// Utilizza la cache automaticamente. /// Task GetRangeAsync(); - /// - /// Inserisce o aggiorna in batch le statistiche aggregate nel database. - /// Opzionalmente elimina i record precedenti nel periodo specificato. - /// - /// Elenco dei record da inserire/aggiornare - /// Se true elimina preventivamente i record nel periodo richiesto - Task UpsertManyAsync(List listRecords, bool removeOld); /// /// Helper conversione dati aggregati in statistiche da inviare a ChartJS /// @@ -56,6 +49,14 @@ namespace MP.Data.Services.Utils /// List GetTimeSeriesData(List rawData, bool groupMach, bool getCount); + /// + /// Inserisce o aggiorna in batch le statistiche aggregate nel database. + /// Opzionalmente elimina i record precedenti nel periodo specificato. + /// + /// Elenco dei record da inserire/aggiornare + /// Se true elimina preventivamente i record nel periodo richiesto + Task UpsertManyAsync(List listRecords, bool removeOld); + #endregion Public Methods } } \ No newline at end of file diff --git a/MP.Data/Services/Utils/IStatsCodeService.cs b/MP.Data/Services/Utils/IStatsCodeService.cs new file mode 100644 index 00000000..10c0239b --- /dev/null +++ b/MP.Data/Services/Utils/IStatsCodeService.cs @@ -0,0 +1,60 @@ +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 IStatsCodeService + { + #region Public Methods + + /// + /// Recupera l'elenco delle statistiche StatusCode per un periodo specificato. + /// Utilizza la cache automaticamente. + /// + /// Data inizio periodo + /// Data fine periodo + Task> GetFiltAsync(DateTime dtStart, DateTime dtEnd); + + /// + /// Restituisce un dizionario di elaborazioni di tipo Pareto su orizzonte giornaliero + /// Ogni elaborazione contiene Dictionary in forma pareto per una data statistica + /// + /// + Task>> GetParetoStatsDayAsync(int numDay); + + /// + /// Restituisce un dizionario di elaborazioni di tipo Pareto su orizzonte settimanale + /// Ogni elaborazione contiene Dictionary in forma pareto per una data statistica + /// + /// + Task>> GetParetoStatsWeekAsync(); + + /// + /// Recupera il range di periodi valido per le chiamate StatusCode. + /// Utilizza la cache automaticamente. + /// + Task GetRangeAsync(); + + /// + /// Helper conversione dati aggregati in statistiche da inviare a ChartJS + /// + /// + /// + List GetTimeSeriesData(List rawData); + + /// + /// Inserisce o aggiorna in batch le statistiche StatusCode nel database. + /// Opzionalmente elimina i record precedenti nel periodo specificato. + /// + /// Elenco dei record da inserire/aggiornare + /// Se true elimina preventivamente i record nel periodo richiesto + Task UpsertManyAsync(List listRecords, bool removeOld); + + #endregion Public Methods + } +} \ No newline at end of file diff --git a/MP.Data/Services/Utils/IStatsErrService.cs b/MP.Data/Services/Utils/IStatsErrService.cs new file mode 100644 index 00000000..3141c7e9 --- /dev/null +++ b/MP.Data/Services/Utils/IStatsErrService.cs @@ -0,0 +1,60 @@ +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 IStatsErrService + { + #region Public Methods + + /// + /// Recupera l'elenco delle statistiche Error per un periodo specificato. + /// Utilizza la cache automaticamente. + /// + /// Data inizio periodo + /// Data fine periodo + Task> GetFiltAsync(DateTime dtStart, DateTime dtEnd); + + /// + /// Restituisce un dizionario di elaborazioni di tipo Pareto su orizzonte giornaliero + /// Ogni elaborazione contiene Dictionary in forma pareto per una data statistica + /// + /// + Task>> GetParetoStatsDayAsync(int numDay); + + /// + /// Restituisce un dizionario di elaborazioni di tipo Pareto su orizzonte settimanale + /// Ogni elaborazione contiene Dictionary in forma pareto per una data statistica + /// + /// + Task>> GetParetoStatsWeekAsync(); + + /// + /// Recupera il range di periodi valido per le chiamate Error. + /// Utilizza la cache automaticamente. + /// + Task GetRangeAsync(); + + /// + /// Helper conversione dati aggregati in statistiche da inviare a ChartJS + /// + /// + /// + List GetTimeSeriesData(List rawData); + + /// + /// Inserisce o aggiorna in batch le statistiche Error nel database. + /// Opzionalmente elimina i record precedenti nel periodo specificato. + /// + /// Elenco dei record da inserire/aggiornare + /// Se true elimina preventivamente i record nel periodo richiesto + Task UpsertManyAsync(List listRecords, bool removeOld); + + #endregion Public Methods + } +} \ No newline at end of file diff --git a/MP.Data/Services/Utils/StatsCodeService.cs b/MP.Data/Services/Utils/StatsCodeService.cs new file mode 100644 index 00000000..3d2bf4df --- /dev/null +++ b/MP.Data/Services/Utils/StatsCodeService.cs @@ -0,0 +1,186 @@ +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 +{ + internal class StatsCodeService : BaseServ, IStatsCodeService + { + #region Public Constructors + + public StatsCodeService( + IConfiguration config, + IConnectionMultiplexer redis, + IStatsCodeRepository repo) : base(config, redis) + { + _className = "StatsStatusCode"; + _repo = repo; + } + + #endregion Public Constructors + + #region Public Methods + + /// + public async Task> 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 + ); + }); + } + + /// + public async Task>> GetParetoStatsDayAsync(int numDay) + { + return await TraceAsync($"{_className}.GetParetoStatsDayAsync", async (activity) => + { + return await GetOrSetCacheAsync( + $"{_redisBaseKey}:{_className}:ParetoDay", + async () => await GetParetoDestAsync(numDay), + LongCache + ); + }); + } + + /// + public async Task>> GetParetoStatsWeekAsync() + { + return await TraceAsync($"{_className}.GetParetoStatsWeekAsync", async (activity) => + { + return await GetOrSetCacheAsync( + $"{_redisBaseKey}:{_className}:ParetoWeek", + async () => await GetParetoDataAsync(), + LongCache + ); + }); + } + + /// + public async Task GetRangeAsync() + { + return await TraceAsync($"{_className}.GetRange", async (activity) => + { + return await GetOrSetCacheAsync( + $"{_redisBaseKey}:{_className}:Range", + async () => await _repo.GetRangeAsync(), + UltraFastCache + ); + }); + } + + /// + public List GetTimeSeriesData(List rawData) + { + DateTime adesso = DateTime.Now; + + List series = new(); + series = rawData + .GroupBy(s => new { s.Destination }) + .Select(group => new ChartSeriesDto + { + SeriesName = group.Key.Destination, + DataPoints = group + .OrderBy(p => p.Hour) + .Select(p => new chartJsData.chartJsTSerie + { + x = p.Hour, + y = p.Count / (p.Hour.Date.Equals(adesso.Date) ? adesso.TimeOfDay.TotalHours : 24) + }) + .ToList() + }) + .ToList(); + + return series; + } + + /// + public async Task UpsertManyAsync(List 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 + + /// + /// metodo locale per recupero e trasformazione dati da includere con processo generare di tracking & cache + /// + /// + protected async Task>> GetParetoDataAsync() + { + Dictionary> result = new(); + DateTime oggi = DateTime.Today; + int numDays = 7; + var rawData = await GetFiltAsync(oggi.AddDays(-numDays), oggi); + // calcolo le varie statistiche... + var pDestRequest = rawData.GroupBy(x => x.Destination) + .Select(g => new StatDataDTO + { + Label = g.Key, + Value = g.Sum(x => x.Count) / numDays + }) + .OrderByDescending(x => x.Value) + .ToList(); + result.Add("Errors (#)", pDestRequest); + + return result; + } + + protected async Task>> GetParetoDestAsync(int numDay) + { + Dictionary> result = new(); + DateTime adesso = DateTime.Now; + DateTime start = DateTime.Today.AddDays(-numDay); + var rawData = await GetFiltAsync(start, adesso); + var numHour = adesso.Subtract(start).TotalHours; + // calcolo le varie statistiche... + var pDestRequest = rawData.GroupBy(x => x.Destination) + .Select(g => new StatDataDTO + { + Label = g.Key, + Value = g.Sum(x => x.Count) / numHour + }) + .OrderByDescending(x => x.Value) + .ToList(); + result.Add("Dest.Request (#/h)", pDestRequest); + + return result; + } + + #endregion Protected Methods + + #region Private Fields + + private readonly string _className; + private readonly IStatsCodeRepository _repo; + + #endregion Private Fields + } +} \ No newline at end of file diff --git a/MP.Data/Services/Utils/StatsErrService.cs b/MP.Data/Services/Utils/StatsErrService.cs new file mode 100644 index 00000000..26e76309 --- /dev/null +++ b/MP.Data/Services/Utils/StatsErrService.cs @@ -0,0 +1,186 @@ +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 StatsErrService : BaseServ, IStatsErrService + { + #region Public Constructors + + public StatsErrService( + IConfiguration config, + IConnectionMultiplexer redis, + IStatsErrRepository repo) : base(config, redis) + { + _className = "StatsErr"; + _repo = repo; + } + + #endregion Public Constructors + + #region Public Methods + + /// + public async Task> 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 + ); + }); + } + + /// + public async Task>> GetParetoStatsDayAsync(int numDay) + { + return await TraceAsync($"{_className}.GetParetoStatsDayAsync", async (activity) => + { + return await GetOrSetCacheAsync( + $"{_redisBaseKey}:{_className}:ParetoDay", + async () => await GetParetoDestAsync(numDay), + LongCache + ); + }); + } + + /// + public async Task>> GetParetoStatsWeekAsync() + { + return await TraceAsync($"{_className}.GetParetoStatsWeekAsync", async (activity) => + { + return await GetOrSetCacheAsync( + $"{_redisBaseKey}:{_className}:ParetoWeek", + async () => await GetParetoDataAsync(), + LongCache + ); + }); + } + + /// + public async Task GetRangeAsync() + { + return await TraceAsync($"{_className}.GetRange", async (activity) => + { + return await GetOrSetCacheAsync( + $"{_redisBaseKey}:{_className}:Range", + async () => await _repo.GetRangeAsync(), + UltraFastCache + ); + }); + } + + /// + public List GetTimeSeriesData(List rawData) + { + DateTime adesso = DateTime.Now; + + List series = new(); + series = rawData + .GroupBy(s => new { s.Destination }) + .Select(group => new ChartSeriesDto + { + SeriesName = group.Key.Destination, + DataPoints = group + .OrderBy(p => p.Hour) + .Select(p => new chartJsData.chartJsTSerie + { + x = p.Hour, + y = p.Count / (p.Hour.Date.Equals(adesso.Date) ? adesso.TimeOfDay.TotalHours : 24) + }) + .ToList() + }) + .ToList(); + + return series; + } + + /// + public async Task UpsertManyAsync(List 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 + + /// + /// metodo locale per recupero e trasformazione dati da includere con processo generare di tracking & cache + /// + /// + protected async Task>> GetParetoDataAsync() + { + Dictionary> result = new(); + DateTime oggi = DateTime.Today; + int numDays = 7; + var rawData = await GetFiltAsync(oggi.AddDays(-numDays), oggi); + // calcolo le varie statistiche... + var pDestRequest = rawData.GroupBy(x => x.Destination) + .Select(g => new StatDataDTO + { + Label = g.Key, + Value = g.Sum(x => x.Count) / numDays + }) + .OrderByDescending(x => x.Value) + .ToList(); + result.Add("Errors (#)", pDestRequest); + + return result; + } + + protected async Task>> GetParetoDestAsync(int numDay) + { + Dictionary> result = new(); + DateTime adesso = DateTime.Now; + DateTime start = DateTime.Today.AddDays(-numDay); + var rawData = await GetFiltAsync(start, adesso); + var numHour = adesso.Subtract(start).TotalHours; + // calcolo le varie statistiche... + var pDestRequest = rawData.GroupBy(x => x.Destination) + .Select(g => new StatDataDTO + { + Label = g.Key, + Value = g.Sum(x => x.Count) / numHour + }) + .OrderByDescending(x => x.Value) + .ToList(); + result.Add("Dest.Request (#/h)", pDestRequest); + + return result; + } + + #endregion Protected Methods + + #region Private Fields + + private readonly string _className; + private readonly IStatsErrRepository _repo; + + #endregion Private Fields + } +} \ No newline at end of file