diff --git a/MP.Data/DbModels/Utils/StatsAggregatedModel.cs b/MP.Data/DbModels/Utils/StatsAggregatedModel.cs new file mode 100644 index 00000000..4221288b --- /dev/null +++ b/MP.Data/DbModels/Utils/StatsAggregatedModel.cs @@ -0,0 +1,47 @@ +// +// This is here so CodeMaid doesn't reorganize this document +// +namespace MP.Data.DbModels.Utils +{ + /// + /// Classe statistiche impiego orario complessive + /// + [Table("stats_aggr")] + public class StatsAggregatedModel + { + [Key] + public int Id { get; set; } + /// + /// DataOra riferimento + /// + public DateTime Hour { get; set; } + /// + /// Num richieste complete/concluse (con ritorno) + /// + public long RequestCount { get; set; } = 0; + /// + /// Durata media esecuzione + /// + public double AvgDuration { get; set; } = 0; + /// + /// Durata massima esecuzione + /// + public double MaxDuration { get; set; } = 0; + /// + /// Durata 5 perc esecuzione + /// + public double Perc05Duration { get; set; } = 0; + /// + /// Durata 95 perc esecuzione + /// + public double Perc95Duration { get; set; } = 0; + /// + /// Durata massima esecuzione + /// + public double MaxDuration { get; set; } = 0; + /// + /// numero richieste senza risposta (num RUID avviati - num RUID chiusi) + /// + public long NoReply { get; set; } = 0; + } +} diff --git a/MP.Data/DbModels/Utils/StatsDetailModel.cs b/MP.Data/DbModels/Utils/StatsDetailModel.cs new file mode 100644 index 00000000..0aa9b4a1 --- /dev/null +++ b/MP.Data/DbModels/Utils/StatsDetailModel.cs @@ -0,0 +1,51 @@ +// +// This is here so CodeMaid doesn't reorganize this document +// +namespace MP.Data.DbModels.Utils +{ + /// + /// Classe statistiche impiego (orarie) di dettaglio (per envir/tipo) + /// + [Table("stats_detail")] + public class StatsDetailModel + { + [Key] + public int Id { get; set; } + /// + /// Ambiente + /// + public string Environment { get; set; } = ""; + /// + /// Codice mode/submode + /// + public string Type { get; set; } = ""; + /// + /// DataOra riferimento + /// + public DateTime Hour { get; set; } + /// + /// Num richieste complete/concluse (con ritorno) + /// + public long RequestCount { get; set; } = 0; + /// + /// Durata media esecuzione + /// + public double AvgDuration { get; set; } = 0; + /// + /// Durata 5 perc esecuzione + /// + public double Perc05Duration { get; set; } = 0; + /// + /// Durata 95 perc esecuzione + /// + public double Perc95Duration { get; set; } = 0; + /// + /// Durata massima esecuzione + /// + public double MaxDuration { get; set; } = 0; + /// + /// numero richieste senza risposta (num RUID avviati - num RUID chiusi) + /// + public long NoReply { get; set; } = 0; + } +} diff --git a/MP.Data/MoonPro_UtilsContext.cs b/MP.Data/MoonPro_UtilsContext.cs new file mode 100644 index 00000000..b87aad6c --- /dev/null +++ b/MP.Data/MoonPro_UtilsContext.cs @@ -0,0 +1,131 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.Extensions.Configuration; +using MP.Core.DTO; +using MP.Data.DbModels; +using MP.Data.DTO; +using NLog; + +#nullable disable +// +// This is here so CodeMaid doesn't reorganize this document +// +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 + + /// + /// Indispensabile x prima generazione migrations EFCore + /// + + [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 options) : base(options) + { + } + + #endregion Public Constructors + + #region Public Properties + + + public virtual DbSet DbSetStatsDet { get; set; } + public virtual DbSet 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() + .HasIndex(x => new { x.Hour, x.Environment, x.Type }) + .HasDatabaseName("idx_statsdet_hour_env_type") + .IsUnique(); + modelBuilder.Entity() + .HasIndex(x => new { x.Hour }) + .HasDatabaseName("idx_statsaggr_hour") + .IsUnique(); + + // modelBuilder.Entity(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(entity => + // { + // entity.HasKey(e => new { e.IdxMacchina, e.CodFlux }); + // }); + + OnModelCreatingPartial(modelBuilder); + } + + #endregion Protected Methods + } +} \ No newline at end of file diff --git a/MP.Data/Repository/Utils/IStatsAggrRepository.cs b/MP.Data/Repository/Utils/IStatsAggrRepository.cs new file mode 100644 index 00000000..5f2bf909 --- /dev/null +++ b/MP.Data/Repository/Utils/IStatsAggrRepository.cs @@ -0,0 +1,32 @@ +using EgwCoreLib.Utils; + +namespace MP.Data.Repository.Utils +{ + /// + /// Interfaccia per le statistiche aggregate. + /// + public interface IStatsAggrRepository + { + /// + /// Recupera l'elenco delle statistiche aggregate per un periodo specifico. + /// + /// La data di inizio del periodo. + /// La data di fine del periodo. + /// L'elenco delle statistiche aggregate ordinate cronologicamente. + Task> GetFiltAsync(DateTime dtStart, DateTime dtEnd); + + /// + /// Recupera l'intervallo temporale disponibile nel database per le statistiche aggregate. + /// + /// L'intervallo di date (minima e massima ora presente). + Task GetRangeAsync(); + + /// + /// Inserisce o aggiorna in blocco le statistiche aggregate nel database. + /// + /// L'elenco dei record da inserire. + /// Se true, elimina preventivamente i record nel periodo richiesto. + /// Il numero di record inseriti. + Task UpsertManyAsync(List listRecords, bool removeOld); + } +} \ No newline at end of file diff --git a/MP.Data/Repository/Utils/IStatsDetailRepository.cs b/MP.Data/Repository/Utils/IStatsDetailRepository.cs new file mode 100644 index 00000000..3db2182c --- /dev/null +++ b/MP.Data/Repository/Utils/IStatsDetailRepository.cs @@ -0,0 +1,36 @@ +using EgwCoreLib.Utils; + +namespace MP.Data.Repository.Utils +{ + /// + /// Interfaccia per le statistiche di dettaglio. + /// + public interface IStatsDetailRepository + { + /// + /// Recupera l'elenco delle statistiche di dettaglio per un periodo specifico, con filtri opzionali. + /// + /// La data di inizio del periodo. + /// La data di fine del periodo. + /// Filtro opzionale per l'ambiente (es. "DEV", "PROD"). + /// Filtro opzionale per il tipo di statistica. + /// L'elenco delle statistiche di dettaglio ordinate cronologicamente. + Task> GetFiltAsync(DateTime dtStart, DateTime dtEnd, string sEnvir = "", string sType = ""); + + /// + /// Recupera l'intervallo temporale disponibile nel database per le statistiche di dettaglio. + /// + /// Filtro opzionale per l'ambiente. + /// Filtro opzionale per il tipo. + /// L'intervallo di date (minima e massima ora presente). + Task GetRangeAsync(string sEnvir, string sType); + + /// + /// Inserisce o aggiorna in blocco le statistiche di dettaglio nel database. + /// + /// L'elenco dei record da inserire. + /// Se true, elimina preventivamente i record nel periodo richiesto. + /// Il numero di record inseriti. + Task UpsertManyAsync(List listRecords, bool removeOld); + } +} \ No newline at end of file diff --git a/MP.Data/Repository/Utils/StatsAggrRepository.cs b/MP.Data/Repository/Utils/StatsAggrRepository.cs new file mode 100644 index 00000000..2f9e6657 --- /dev/null +++ b/MP.Data/Repository/Utils/StatsAggrRepository.cs @@ -0,0 +1,93 @@ +using EgwCoreLib.Utils; + +namespace MP.Data.Repository.Utils +{ + public class StatsAggrRepository : BaseRepository, IStatsAggrRepository + { + #region Public Constructors + + public StatsAggrRepository(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 + .DbSetStatsAggr + .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.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; + } + + /// + public async Task UpsertManyAsync(List 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; + // uso direttamente ExecuteDelete + await dbCtx + .DbSetStatsAggr + .Where(x => x.Hour >= startDate && x.Hour <= endDate) + .ExecuteDeleteAsync(); + } + } + + // ora preparo inserimento massivo + 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; + } + } + + #endregion Public Methods + } +} \ No newline at end of file diff --git a/MP.Data/Repository/Utils/StatsDetailRepository.cs b/MP.Data/Repository/Utils/StatsDetailRepository.cs new file mode 100644 index 00000000..4b09d801 --- /dev/null +++ b/MP.Data/Repository/Utils/StatsDetailRepository.cs @@ -0,0 +1,113 @@ +using EgwCoreLib.Utils; + +namespace MP.Data.Repository.Utils +{ + public class StatsDetailRepository : BaseRepository, IStatsDetailRepository + { + #region Public Constructors + + public StatsDetailRepository(IDbContextFactory ctxFactory) : base(ctxFactory) + { + } + + #endregion Public Constructors + + #region Internal Methods + + /// + public async Task> GetFiltAsync(DateTime dtStart, DateTime dtEnd, string sEnvir = "", string sType = "") + { + await using var dbCtx = await CreateContextAsync(); + List answ = new List(); + + // 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.Environment == sEnvir); + + if (!string.IsNullOrEmpty(sType)) + query = query.Where(x => x.Type == sType); + + answ = await query + .AsNoTracking() + .OrderBy(x => x.Hour) + .ThenBy(x => x.Environment) + .ThenBy(x => x.Type) + .ToListAsync(); + + return answ; + } + + /// + public async Task 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.Environment == 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; + } + + /// + public async Task UpsertManyAsync(List 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; + // uso direttamente ExecuteDelete + await dbCtx + .DbSetStatsDet + .Where(x => x.Hour >= startDate && x.Hour <= endDate) + .ExecuteDeleteAsync(); + } + } + + // ora preparo inserimento massivo + 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; + } + } + + #endregion Internal Methods + } +} \ No newline at end of file diff --git a/MP.Data/Services/Utils/IStatsAggrService.cs b/MP.Data/Services/Utils/IStatsAggrService.cs new file mode 100644 index 00000000..d38a0403 --- /dev/null +++ b/MP.Data/Services/Utils/IStatsAggrService.cs @@ -0,0 +1,31 @@ +namespace MP.Data.Services.Utils +{ + public interface IStatsAggrService + { + #region Public Methods + + /// + /// Recupera l'elenco delle statistiche aggregate per un periodo specificato. + /// Utilizza la cache automaticamente. + /// + /// Data inizio periodo + /// Data fine periodo + Task> GetFiltAsync(DateTime dtStart, DateTime dtEnd); + + /// + /// 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); + + #endregion Public Methods + } +} \ No newline at end of file diff --git a/MP.Data/Services/Utils/IStatsDetailService.cs b/MP.Data/Services/Utils/IStatsDetailService.cs new file mode 100644 index 00000000..1eb399da --- /dev/null +++ b/MP.Data/Services/Utils/IStatsDetailService.cs @@ -0,0 +1,35 @@ +namespace MP.Data.Services.Utils +{ + public interface IStatsDetailService + { + #region Public Methods + + /// + /// Recupera le statistiche di dettaglio per un periodo specificato, con filtri opzionali su ambiente e tipo. + /// Utilizza la cache automaticamente. + /// + /// Data inizio periodo + /// Data fine periodo + /// Filtro opzionale per ambiente (default: vuoto = tutti) + /// Filtro opzionale per tipo (default: vuoto = tutti) + Task> GetFiltAsync(DateTime dtStart, DateTime dtEnd, string sEnvir = "", string sType = ""); + + /// + /// Recupera il range di periodi valido per le chiamate di dettaglio. + /// Utilizza la cache automaticamente. + /// + /// Ambiente filtrato + /// Tipo filtrato + Task GetRangeAsync(string sEnvir, string sType); + + /// + /// Inserisce o aggiorna in batch le statistiche di dettaglio 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/StatsAggrService.cs b/MP.Data/Services/Utils/StatsAggrService.cs new file mode 100644 index 00000000..2d6ec1ff --- /dev/null +++ b/MP.Data/Services/Utils/StatsAggrService.cs @@ -0,0 +1,74 @@ +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 + + /// + 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 GetRangeAsync() + { + return await TraceAsync($"{_className}.GetRange", async (activity) => + { + return await GetOrSetCacheAsync( + $"{_redisBaseKey}:{_className}:Range", + async () => await _repo.GetRangeAsync(), + UltraFastCache + ); + }); + } + + /// + 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 Private Fields + + private readonly string _className; + private readonly IStatsAggrRepository _repo; + + #endregion Private Fields + } +} \ No newline at end of file diff --git a/MP.Data/Services/Utils/StatsDetailService.cs b/MP.Data/Services/Utils/StatsDetailService.cs new file mode 100644 index 00000000..43a9d2fe --- /dev/null +++ b/MP.Data/Services/Utils/StatsDetailService.cs @@ -0,0 +1,74 @@ +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 + + /// + public async Task> GetFiltAsync(DateTime dtStart, DateTime dtEnd, string sEnvir = "", string sType = "") + { + 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 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 + ); + }); + } + + /// + 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 Private Fields + + private readonly string _className; + private readonly IStatsDetailRepository _repo; + + #endregion Private Fields + } +} \ No newline at end of file diff --git a/MP.IOC/appsettings.json b/MP.IOC/appsettings.json index 042c4fef..5587b2d4 100644 --- a/MP.IOC/appsettings.json +++ b/MP.IOC/appsettings.json @@ -70,9 +70,7 @@ } }, "RouteMan": { - "FlushIntervalSeconds": 10, - //"FlushIntervalSeconds": 60, - //"FlushIntervalSeconds": 300, + "FlushIntervalSeconds": 60, "DefaultWeightOld": 100, "DefaultWeightNew": 0 }, @@ -84,6 +82,7 @@ }, "ConnectionStrings": { "MP.Data": "Server=SQL2016DEV;Database=MoonPro; User ID=sa;Password=keyhammer16; integrated security=False; MultipleActiveResultSets=True; App=MP.IOC;", + "MP.Utils": "Server=SQL2016DEV;Database=MoonPro_Utils; User ID=sa;Password=keyhammer16; integrated security=False; MultipleActiveResultSets=True; App=MP.IOC;", "Redis": "redis.ufficio:26379,serviceName=devel,DefaultDatabase=5,connectTimeout=5000,syncTimeout=5000,asyncTimeout=5000,abortConnect=false,ssl=false", "RedisAdmin": "redis.ufficio:26379,serviceName=devel,DefaultDatabase=5,connectTimeout=5000,syncTimeout=5000,asyncTimeout=5000,abortConnect=false,ssl=false,allowAdmin=true", "mdbConnString": "mongodb://W2019-MONGODB:27017"