Fix integrazione preliminare servizi utils.stats x IOC:

- aggiunta migrations
- correzioni versione ef6 da ef8
- correzioni init varie
This commit is contained in:
Samuele Locatelli
2026-04-07 10:30:04 +02:00
parent 31e786b9fd
commit 45cb6b9f59
32 changed files with 1290 additions and 362 deletions
+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>
+7 -11
View File
@@ -1,4 +1,8 @@
// <Auto-Generated>
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
@@ -24,17 +28,9 @@ namespace MP.Data.DbModels.Utils
/// </summary>
public double AvgDuration { get; set; } = 0;
/// <summary>
/// Durata massima esecuzione
/// Durata minima esecuzione
/// </summary>
public double MaxDuration { get; set; } = 0;
/// <summary>
/// Durata 5 perc esecuzione
/// </summary>
public double Perc05Duration { get; set; } = 0;
/// <summary>
/// Durata 95 perc esecuzione
/// </summary>
public double Perc95Duration { get; set; } = 0;
public double MinDuration { get; set; } = 0;
/// <summary>
/// Durata massima esecuzione
/// </summary>
+7 -7
View File
@@ -1,4 +1,8 @@
// <Auto-Generated>
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
@@ -32,13 +36,9 @@ namespace MP.Data.DbModels.Utils
/// </summary>
public double AvgDuration { get; set; } = 0;
/// <summary>
/// Durata 5 perc esecuzione
/// Durata minima esecuzione
/// </summary>
public double Perc05Duration { get; set; } = 0;
/// <summary>
/// Durata 95 perc esecuzione
/// </summary>
public double Perc95Duration { get; set; } = 0;
public double MinDuration { get; set; } = 0;
/// <summary>
/// Durata massima esecuzione
/// </summary>
@@ -0,0 +1,107 @@
// <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("20260407082127_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<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("Hour")
.IsUnique()
.HasDatabaseName("idx_statsaggr_hour");
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>("Environment")
.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("Hour", "Environment", "Type")
.IsUnique()
.HasDatabaseName("idx_statsdet_hour_env_type")
.HasFilter("[Environment] IS NOT NULL AND [Type] IS NOT NULL");
b.ToTable("stats_detail");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,73 @@
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"),
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"),
Environment = 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_hour",
table: "stats_aggr",
column: "Hour",
unique: true);
migrationBuilder.CreateIndex(
name: "idx_statsdet_hour_env_type",
table: "stats_detail",
columns: new[] { "Hour", "Environment", "Type" },
unique: true,
filter: "[Environment] 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,105 @@
// <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<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("Hour")
.IsUnique()
.HasDatabaseName("idx_statsaggr_hour");
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>("Environment")
.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("Hour", "Environment", "Type")
.IsUnique()
.HasDatabaseName("idx_statsdet_hour_env_type")
.HasFilter("[Environment] IS NOT NULL AND [Type] IS NOT NULL");
b.ToTable("stats_detail");
});
#pragma warning restore 612, 618
}
}
}
+7 -10
View File
@@ -1,11 +1,8 @@
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using MP.Core.DTO;
using MP.Data.DbModels;
using MP.Data.DTO;
using MP.Data.DbModels.Utils;
using NLog;
using System;
#nullable disable
// <Auto-Generated>
@@ -34,10 +31,10 @@ namespace MP.Data
{
}
public MoonPro_UtilsContext(IConfiguration configuration)
{
_configuration = configuration;
}
//public MoonPro_UtilsContext(IConfiguration configuration)
//{
// _configuration = configuration;
//}
public MoonPro_UtilsContext(DbContextOptions<MoonPro_UtilsContext> options) : base(options)
{
+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();
#if false
/// <summary>
/// Salvataggio dati asincrono
/// </summary>
/// <returns></returns>
protected async Task<bool> SaveChangesAsync(DataLayerContext ctx)
=> await ctx.SaveChangesAsync() > 0;
#endif
#endregion Protected Methods
#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);
}
}
@@ -1,4 +1,8 @@
using EgwCoreLib.Utils;
using MP.Data.DbModels.Utils;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace MP.Data.Repository.Utils
{
@@ -1,4 +1,8 @@
using EgwCoreLib.Utils;
using MP.Data.DbModels.Utils;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace MP.Data.Repository.Utils
{
@@ -1,4 +1,10 @@
using EgwCoreLib.Utils;
using Microsoft.EntityFrameworkCore;
using MP.Data.DbModels.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace MP.Data.Repository.Utils
{
@@ -6,7 +12,7 @@ namespace MP.Data.Repository.Utils
{
#region Public Constructors
public StatsAggrRepository(IDbContextFactory<DataLayerContext> ctxFactory) : base(ctxFactory)
public StatsAggrRepository(IDbContextFactory<MoonPro_UtilsContext> ctxFactory) : base(ctxFactory)
{
}
@@ -58,11 +64,20 @@ namespace MP.Data.Repository.Utils
{
DateTime startDate = firstRec.Hour;
DateTime endDate = lastRec.Hour;
// uso direttamente ExecuteDelete
// uso direttamente ExecuteDelete quando in EFCore8...
#if false
await dbCtx
.DbSetStatsAggr
.Where(x => x.Hour >= startDate && x.Hour <= endDate)
.ExecuteDeleteAsync();
#endif
var items = await dbCtx.DbSetStatsAggr
.Where(x => x.Hour >= startDate && x.Hour <= endDate)
.ToListAsync();
dbCtx.DbSetStatsAggr.RemoveRange(items);
await dbCtx.SaveChangesAsync();
}
}
@@ -1,4 +1,10 @@
using EgwCoreLib.Utils;
using Microsoft.EntityFrameworkCore;
using MP.Data.DbModels.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace MP.Data.Repository.Utils
{
@@ -6,7 +12,7 @@ namespace MP.Data.Repository.Utils
{
#region Public Constructors
public StatsDetailRepository(IDbContextFactory<DataLayerContext> ctxFactory) : base(ctxFactory)
public StatsDetailRepository(IDbContextFactory<MoonPro_UtilsContext> ctxFactory) : base(ctxFactory)
{
}
@@ -79,11 +85,20 @@ namespace MP.Data.Repository.Utils
{
DateTime startDate = firstRec.Hour;
DateTime endDate = lastRec.Hour;
// uso direttamente ExecuteDelete
await dbCtx
.DbSetStatsDet
.Where(x => x.Hour >= startDate && x.Hour <= endDate)
.ExecuteDeleteAsync();
// uso direttamente ExecuteDelete quando in EFCore8...
#if false
await dbCtx
.DbSetStatsDet
.Where(x => x.Hour >= startDate && x.Hour <= endDate)
.ExecuteDeleteAsync();
#endif
var items = await dbCtx.DbSetStatsDet
.Where(x => x.Hour >= startDate && x.Hour <= endDate)
.ToListAsync();
dbCtx.DbSetStatsDet.RemoveRange(items);
await dbCtx.SaveChangesAsync();
}
}
+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);
+2 -2
View File
@@ -688,7 +688,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 +745,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);
+7 -1
View File
@@ -1,4 +1,10 @@
namespace MP.Data.Services.Utils
using EgwCoreLib.Utils;
using MP.Data.DbModels.Utils;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace MP.Data.Services.Utils
{
public interface IStatsAggrService
{
@@ -1,4 +1,10 @@
namespace MP.Data.Services.Utils
using EgwCoreLib.Utils;
using MP.Data.DbModels.Utils;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace MP.Data.Services.Utils
{
public interface IStatsDetailService
{
+10 -1
View File
@@ -1,4 +1,13 @@
namespace MP.Data.Services.Utils
using EgwCoreLib.Utils;
using Microsoft.Extensions.Configuration;
using MP.Data.DbModels.Utils;
using MP.Data.Repository.Utils;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace MP.Data.Services.Utils
{
public class StatsAggrService : BaseServ, IStatsAggrService
{
+9 -1
View File
@@ -1,4 +1,12 @@
namespace MP.Data.Services.Utils
using EgwCoreLib.Utils;
using Microsoft.Extensions.Configuration;
using MP.Data.DbModels.Utils;
using MP.Data.Repository.Utils;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace MP.Data.Services.Utils
{
public class StatsDetailService : BaseServ, IStatsDetailService
{
+10 -3
View File
@@ -1,5 +1,7 @@
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.Data;
@@ -60,11 +62,16 @@ logger.Info("YARP reverse proxy configured");
builder.Services.AddSingleton<PreserveBodyTransformer>();
builder.Services.AddSingleton<RouteStatsManager>();
builder.Services.AddHostedService<MetricsFlushService>();
builder.Services.AddHostedService<MetricsDbFlushService>();
// da rivedere
#if false
builder.Services.AddHostedService<MetricsDbFlushService>();
#endif
// MP.Data DbContext for Stats repositories
builder.Services.AddDbContextFactory<DataLayerContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("MP.Utils")));
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>();
+1 -1
View File
@@ -1,6 +1,6 @@
<body>
<i>Modulo MP-IOC </i>
<h4>Versione: 6.16.2604.410</h4>
<h4>Versione: 6.16.2604.710</h4>
<br /> Note di rilascio:
<ul>
<li>
+1 -1
View File
@@ -1 +1 @@
6.16.2604.410
6.16.2604.710
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>6.16.2604.410</version>
<version>6.16.2604.710</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
}
}
+5 -8
View File
@@ -1,7 +1,6 @@
using MP.Data.DbModels.Utils;
using MP.Data.Services.Utils;
using NLog;
using System.Globalization;
namespace MP.IOC.Services
{
@@ -29,7 +28,7 @@ namespace MP.IOC.Services
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var interval = _config.GetValue<int>("RouteMan:FlushIntervalSeconds", FlushIntervalSeconds);
while (!stoppingToken.IsCancellationRequested)
{
try
@@ -51,7 +50,7 @@ namespace MP.IOC.Services
public async Task FlushMetricsAsync()
{
var snapshot = _stats.Snapshot();
if (snapshot.Count == 0)
{
return;
@@ -61,7 +60,7 @@ namespace MP.IOC.Services
{
var utcNow = DateTime.Now;
var hourStart = new DateTime(utcNow.Year, utcNow.Month, utcNow.Day, utcNow.Hour, 0, 0);
var aggrRecords = new List<StatsAggregatedModel>();
var detailRecords = new List<StatsDetailModel>();
@@ -82,8 +81,7 @@ namespace MP.IOC.Services
RequestCount = count,
AvgDuration = avgDuration,
MaxDuration = totalMs,
Perc05Duration = 0,
Perc95Duration = 0,
MinDuration = 0,
NoReply = 0
};
@@ -99,8 +97,7 @@ namespace MP.IOC.Services
RequestCount = dest.Value,
AvgDuration = totalMs / dest.Value,
MaxDuration = totalMs,
Perc05Duration = 0,
Perc95Duration = 0,
MinDuration = 0,
NoReply = 0
};