diff --git a/Directory.Packages.props b/Directory.Packages.props index 71c44dc1..aee39b65 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -24,7 +24,7 @@ - + diff --git a/MP.Data/Controllers/MpSpecController.cs b/MP.Data/Controllers/MpSpecController.cs index 9847a5ad..cb8d41ef 100644 --- a/MP.Data/Controllers/MpSpecController.cs +++ b/MP.Data/Controllers/MpSpecController.cs @@ -342,9 +342,9 @@ namespace MP.Data.Controllers /// Elenco valori ammessi x Stati commessa (es Yacht Baglietto) /// /// - public List AnagStatiComm() + public Task> AnagStatiCommAsync() { - return ListValuesFilt("PODL", "StatoComm"); + return ListValuesFiltAsync("PODL", "StatoComm"); } /// @@ -1522,6 +1522,27 @@ namespace MP.Data.Controllers return dbResult; } + /// + /// Elenco valori ammessi x tabella/colonna Async + /// + /// + /// + /// + public async Task> ListValuesFiltAsync(string tabName, string fieldName) + { + List dbResult = new List(); + using (var dbCtx = new MoonProContext(options)) + { + dbResult = await dbCtx + .DbSetListValues + .Where(x => x.TableName == tabName && x.FieldName == fieldName) + .AsNoTracking() + .OrderBy(x => x.ordinal) + .ToListAsync(); + } + return dbResult; + } + /// /// Elenco Macchine dato operatore secondo gruppi (macchine/operatore) /// diff --git a/MP.SPEC/Data/MpDataService.cs b/MP.SPEC/Data/MpDataService.cs index 4014b17d..8370b7cf 100644 --- a/MP.SPEC/Data/MpDataService.cs +++ b/MP.SPEC/Data/MpDataService.cs @@ -1,6 +1,5 @@ using EgwCoreLib.Utils; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Caching.Memory; using MP.Core.Conf; using MP.Core.DTO; using MP.Core.Objects; @@ -14,6 +13,7 @@ using NLog; using StackExchange.Redis; using System.Data; using System.Diagnostics; +using ZiggyCreatures.Caching.Fusion; namespace MP.SPEC.Data { @@ -21,11 +21,11 @@ namespace MP.SPEC.Data { #region Public Constructors - public MpDataService(IConfiguration configuration, IMemoryCache memoryCache) + public MpDataService(IConfiguration configuration, IFusionCache cache) { // salvataggio oggetti _configuration = configuration; - _memoryCache = memoryCache; + _cache = cache; // Verifica conf trace... traceEnabled = _configuration.GetValue("Otel:EnableTracing", false); Log.Info($"MpDataService | INIT | Trace enabled: {traceEnabled}"); @@ -300,15 +300,16 @@ namespace MP.SPEC.Data LogTrace($"AnagKeyValGetAll Read from {source}: {activity?.Duration.TotalMilliseconds}ms"); return result; } + public async Task> AnagStatiComm() { - return await GetOrCreateCachedAsync( - operationName: "AnagStatiComm", - memKey: "ANAG_STATI_COMM_MEM", - redisKey: Utils.redisStatoCom, - memoryTtl: TimeSpan.FromMinutes(5), - dbFactory: () => Task.FromResult(dbController.AnagStatiComm() ?? new List()) - ); + return await GetOrFetchAsync( + operationName: "AnagStatiCommAsync", + cacheKey: Utils.redisStatoCom, + expiration: TimeSpan.FromMinutes(5), + fetchFunc: async () => + await dbController.AnagStatiCommAsync() ?? new List() + ); } #if false @@ -345,7 +346,7 @@ namespace MP.SPEC.Data public async Task> AnagTipoArtLV() { - using var activity = ActivitySource.StartActivity("AnagStatiComm"); + using var activity = ActivitySource.StartActivity("AnagStatiCommAsync"); string source = "DB"; List? result = new List(); // cerco in redis... @@ -438,16 +439,28 @@ namespace MP.SPEC.Data string redisKey = $"{Utils.redisArtList}:{azienda}:Tipo:{sKey}"; string memKey = $"MEM:{redisKey}"; - return await GetOrCreateCachedAsync( - operationName: "ArticoliGetByTipoAsync", - memKey: memKey, - redisKey: redisKey, - // ✅ TTL lungo (dato abbastanza statico) - memoryTtl: TimeSpan.FromMinutes(5), - dbFactory: async () => - await dbController.ArticoliGetByTipoAsync(tipo, azienda) + return await GetOrFetchAsync( + operationName: "ArticoliGetByTipoAsync", + cacheKey: redisKey, + expiration: TimeSpan.FromMinutes(2), + fetchFunc: async () => + await dbController.ArticoliGetByTipoAsync(tipo, azienda) ?? new List() - ); + ); + + +#if false + return await GetOrCreateCachedAsync( + operationName: "ArticoliGetByTipoAsync", + memKey: memKey, + redisKey: redisKey, + // ✅ TTL lungo (dato abbastanza statico) + memoryTtl: TimeSpan.FromMinutes(5), + dbFactory: async () => + await dbController.ArticoliGetByTipoAsync(tipo, azienda) + ?? new List() + ); +#endif } #if false public async Task> ArticoliGetByTipoAsync(string tipo, string azienda = "*") @@ -496,18 +509,161 @@ namespace MP.SPEC.Data string memKey = $"ART_SEARCH_MEM:{azienda}:{sKey}:{numRecord}"; string redisKey = $"{Utils.redisArtList}:{azienda}:{sKey}:{numRecord}"; - return await GetOrCreateCachedAsync( - operationName: "ArticoliGetSearchAsync", - memKey: memKey, - redisKey: redisKey, - memoryTtl: TimeSpan.FromMinutes(2), - dbFactory: async () => - await dbController.ArticoliGetSearchAsync(numRecord, azienda, searchVal) + return await GetOrFetchAsync( + operationName: "ArticoliGetSearchAsync", + cacheKey: redisKey, + expiration: TimeSpan.FromMinutes(2), + fetchFunc: async () => + await dbController.ArticoliGetSearchAsync(numRecord, azienda, searchVal) ?? new List() - ); + ); + +#if false + return await GetOrCreateCachedAsync( + operationName: "ArticoliGetSearchAsync", + memKey: memKey, + redisKey: redisKey, + memoryTtl: TimeSpan.FromMinutes(2), + dbFactory: async () => + await dbController.ArticoliGetSearchAsync(numRecord, azienda, searchVal) + ?? new List() + ); +#endif } + /// + /// Implementa gestione recupero cache da memoria o da obj esterno + cache memoria + tracking attività + /// + /// + /// + /// + /// + /// + private async Task GetOrFetchAsync(string operationName, string cacheKey, Func> fetchFunc, TimeSpan expiration, params string[] tags) + { + using var activity = ActivitySource.StartActivity(operationName); + string source; + var tryGet = await _cache.TryGetAsync(cacheKey); + if (tryGet.HasValue) + { + source = "MEMORY"; + var result = tryGet.Value!; + activity?.SetTag("data.source", source); + LogTrace($"{operationName} | {source} | {activity?.Duration.TotalMilliseconds:F4} ms"); + return result; + } + bool fromDb = false; + var final = await _cache.GetOrSetAsync( + cacheKey, + async _ => + { + fromDb = true; + return await fetchFunc(); + }, + opt => + { + opt.SetDuration(expiration) + .SetFailSafe(true); + + //if (tags != null && tags.Length > 0) + // opt.SetTags(tags); + + }); + + source = fromDb ? "DB" : "REDIS"; + activity?.SetTag("data.source", source); + LogTrace($"{operationName} | {source} | {activity?.Duration.TotalMilliseconds:F4} ms"); + return final!; + } + +#if false + private async Task GetOrFetchAsync(string operationName, string cacheKey, Func> fetchFunc, TimeSpan expiration) + { + using var activity = ActivitySource.StartActivity(operationName); + + string source; + T result; + + // ✅ 1. Tenta MEMORY / DISTRIBUTED (senza factory) + var memTry = await _cache.TryGetAsync(cacheKey); + + if (memTry.HasValue) + { + result = memTry.Value!; + source = "MEMORY"; + } + else + { + bool fromDb = false; + + // ✅ 2. fallback con factory + result = await _cache.GetOrSetAsync( + cacheKey, + async _ => + { + fromDb = true; + return await fetchFunc(); + }, + options => options.SetDuration(expiration) + )!; + + source = fromDb ? "DB" : "REDIS"; + } + + // ✅ tracing e log + activity?.SetTag("data.source", source); + + if (result is System.Collections.ICollection coll) + activity?.SetTag("result.count", coll.Count); + else + activity?.SetTag("result.count", result != null ? 1 : 0); + + LogTrace($"{operationName} | {source} | {activity?.Duration.TotalMilliseconds:F4} ms"); + + return result; + } +#endif + + /// + /// Cancellazione FusionCache (totale) + /// + /// + public async Task FlushCacheAsync() + { + bool fatto = false; + await _cache.ClearAsync(); + fatto = true; + return fatto; + } + + /// + /// Cancellazione FusionCache dato elenco tags + /// + /// + public async Task FlushCacheByTagsAsync(List listTags) + { + bool fatto = false; + foreach (var item in listTags) + { + await _cache.RemoveByTagAsync(item); + } + fatto = true; + return fatto; + } + /// + /// Cancellazione FusionCache dato singolo tag + /// + /// + public async Task FlushCacheByTagAsync(string tag) + { + bool fatto = false; + await _cache.RemoveByTagAsync(tag); + fatto = true; + return fatto; + } + +#if false /// /// Helper per gestione cache a 3 livelli: MEMORY, REDIS e DB con opzioni /// @@ -527,12 +683,20 @@ namespace MP.SPEC.Data string source = "NA"; T result; + string groupKey = memKey.Split(':')[0]; // es: "MACCHINE_MEM" + + _memoryKeys.AddOrUpdate( + groupKey, + _ => new HashSet { memKey }, + (_, set) => { set.Add(memKey); return set; } + ); + // ✅ default serializer (fallback) serialize ??= (obj) => JsonConvert.SerializeObject(obj); deserialize ??= (str) => JsonConvert.DeserializeObject(str)!; // ✅ 1. MEMORY - if (_memoryCache.TryGetValue(memKey, out T cached)) + if (_cache.TryGetValue(memKey, out T cached)) { result = cached; source = "MEMORY"; @@ -540,24 +704,17 @@ namespace MP.SPEC.Data else { // ✅ 2. MISS → factory - result = await _memoryCache.GetOrCreateAsync(memKey, async entry => + result = await _cache.GetOrCreateAsync(memKey, async entry => { entry.AbsoluteExpirationRelativeToNow = memoryTtl; // 👉 REDIS - try - { - var rawData = await redisDb.StringGetAsync(redisKey); + var rawData = await redisDb.StringGetAsync(redisKey); - if (rawData.HasValue) - { - source = "REDIS"; - return deserialize(rawData!); - } - } - catch (Exception ex) + if (rawData.HasValue) { - LogTrace($"Redis error on {operationName}: {ex.Message}"); + source = "REDIS"; + return deserialize(rawData!); } // 👉 DB @@ -567,18 +724,11 @@ namespace MP.SPEC.Data var safeResult = dbResult == null ? default! : dbResult; - try - { - await redisDb.StringSetAsync( - redisKey, - serialize(safeResult), - getRandTOut(redisLongTimeCache) - ); - } - catch (Exception ex) - { - LogTrace($"Redis SET error on {operationName}: {ex.Message}"); - } + await redisDb.StringSetAsync( + redisKey, + serialize(safeResult), + getRandTOut(redisLongTimeCache) + ); return safeResult; })!; @@ -595,8 +745,64 @@ namespace MP.SPEC.Data LogTrace($"{operationName} | {source} | {activity?.Duration.TotalMilliseconds}ms"); return result; - } + } + /// + /// Invalidazione di una chiave in memoria e Redis + /// + /// + /// + /// + public async Task InvalidateCacheAsync(string memKey, string redisKey) + { + // ✅ memoria + _cache.Remove(memKey); + + // ✅ redis + await redisDb.KeyDeleteAsync(redisKey); + + LogTrace($"Cache invalidated | {memKey}"); + } + private readonly ConcurrentDictionary> _memoryKeys = new(); + /// + /// Invalidazione cache da pattern + /// + /// + /// + public async Task InvalidateCacheByPatternAsync(string pattern) + { + // ✅ MEMORY (pattern match semplice) + var keysToRemove = _memoryKeys.Keys + .Where(k => k.Contains(pattern.Replace("*", ""))) + .ToList(); + + foreach (var key in keysToRemove) + { + if (_memoryKeys.TryRemove(key, out var subKeys)) + { + foreach (var k in subKeys) + { + _cache.Remove(k); + } + } + } + + // ✅ REDIS + var masterEndpoint = redisConn.GetEndPoints() + .Where(ep => redisConn.GetServer(ep).IsConnected && !redisConn.GetServer(ep).IsReplica) + .FirstOrDefault(); + var server = redisConn.GetServer(masterEndpoint); + + var redisKeys = server.Keys(pattern: pattern); + + foreach (var rk in redisKeys) + { + await redisDb.KeyDeleteAsync(rk); + } + + LogTrace($"Cache invalidated by pattern | {pattern}"); + } +#endif /// /// Aggiornamento record selezionato @@ -1798,7 +2004,7 @@ namespace MP.SPEC.Data LogTrace($"MacchineGetFilt | Read from {source}: {activity?.Duration.TotalMilliseconds}ms"); return result; } - private readonly IMemoryCache _memoryCache; + private readonly IFusionCache _cache; /// /// Elenco di tutte le macchine filtrate x gruppo /// @@ -1811,16 +2017,27 @@ namespace MP.SPEC.Data string redisKey = $"{Utils.redisMacList}:{keyGrp}"; string memKey = $"MACCHINE_MEM:{keyGrp}"; - return await GetOrCreateCachedAsync( - operationName: "MacchineGetFiltAsync", - memKey: memKey, - redisKey: redisKey, - // ✅ TTL coerente con il tuo requisito (prima avevi 1 minuto) - memoryTtl: TimeSpan.FromMinutes(1), - dbFactory: async () => - await dbController.MacchineGetFiltAsync(codGruppo) + return await GetOrFetchAsync( + operationName: "MacchineGetFiltAsync", + cacheKey: redisKey, + expiration: TimeSpan.FromMinutes(5), + fetchFunc: async () => + await dbController.MacchineGetFiltAsync(codGruppo) ?? new List() - ); + ); + +#if false + return await GetOrCreateCachedAsync( + operationName: "MacchineGetFiltAsync", + memKey: memKey, + redisKey: redisKey, + // ✅ TTL coerente con il tuo requisito (prima avevi 1 minuto) + memoryTtl: TimeSpan.FromMinutes(1), + dbFactory: async () => + await dbController.MacchineGetFiltAsync(codGruppo) + ?? new List() + ); +#endif } #if false public async Task> MacchineGetFiltAsync(string codGruppo) @@ -2196,23 +2413,41 @@ namespace MP.SPEC.Data string redisKey = Utils.redisOdlCurrByMac; string memKey = $"MEM:{redisKey}"; - return await GetOrCreateCachedAsync( - operationName: "OdlGetCurrentAsync", - memKey: memKey, - redisKey: redisKey, - // ✅ TTL molto corto (come avevi: 3 secondi) - memoryTtl: TimeSpan.FromSeconds(3), - dbFactory: async () => - { - var rawData = await dbController.OdlGetCurrentAsync(); - var dbResult = rawData - .Select(x => x.IdxMacchina) - .Distinct() - .ToList(); + return await GetOrFetchAsync( + operationName: "OdlGetCurrentAsync", + cacheKey: redisKey, + expiration: TimeSpan.FromSeconds(3), + fetchFunc: async () => + { + var rawData = await dbController.OdlGetCurrentAsync(); + var dbResult = rawData + .Select(x => x.IdxMacchina) + .Distinct() + .ToList(); - return dbResult ?? new List(); - } - ); + return dbResult ?? new List(); + } + ); + +#if false + return await GetOrCreateCachedAsync( + operationName: "OdlGetCurrentAsync", + memKey: memKey, + redisKey: redisKey, + // ✅ TTL molto corto (come avevi: 3 secondi) + memoryTtl: TimeSpan.FromSeconds(3), + dbFactory: async () => + { + var rawData = await dbController.OdlGetCurrentAsync(); + var dbResult = rawData + .Select(x => x.IdxMacchina) + .Distinct() + .ToList(); + + return dbResult ?? new List(); + } + ); +#endif } #if false @@ -2658,18 +2893,16 @@ namespace MP.SPEC.Data /// public async Task> POdlToKitListGetFiltAsync(bool lanciato, string keyRichPart, string idxMacchina, string codGruppo, DateTime startDate, DateTime endDate) { - string currKey = $"{Utils.redisPOdlList}_kit:{codGruppo}:{idxMacchina}:{keyRichPart}:{lanciato}:{startDate:yyyyMMdd_HHmmss}:{endDate:yyyyMMdd_HHmmss}"; + string redisKey = $"{Utils.redisPOdlList}_kit:{codGruppo}:{idxMacchina}:{keyRichPart}:{lanciato}:{startDate:yyyyMMdd_HHmmss}:{endDate:yyyyMMdd_HHmmss}"; // ✅ stessa chiave per memoria (puoi anche prefissare) - string memKey = $"MEM:{currKey}"; + string memKey = $"MEM:{redisKey}"; - return await GetOrCreateCachedAsync( - operationName: "POdlToKitListGetFiltAsync", - memKey: memKey, - redisKey: currKey, - // ✅ TTL RAM breve (coerente con redisShortTimeCache) - memoryTtl: TimeSpan.FromSeconds(redisShortTimeCache), - dbFactory: async () => + return await GetOrFetchAsync( + operationName: "POdlToKitListGetFiltAsync", + cacheKey: redisKey, + expiration: TimeSpan.FromSeconds(redisShortTimeCache), + fetchFunc: async () => await dbController.ListPODL_KitFiltAsync( lanciato, keyRichPart, @@ -2678,7 +2911,26 @@ namespace MP.SPEC.Data startDate, endDate ) ?? new List() - ); + ); + +#if false + return await GetOrCreateCachedAsync( + operationName: "POdlToKitListGetFiltAsync", + memKey: memKey, + redisKey: redisKey, + // ✅ TTL RAM breve (coerente con redisShortTimeCache) + memoryTtl: TimeSpan.FromSeconds(redisShortTimeCache), + dbFactory: async () => + await dbController.ListPODL_KitFiltAsync( + lanciato, + keyRichPart, + idxMacchina, + codGruppo, + startDate, + endDate + ) ?? new List() + ); +#endif } #if false public async Task> POdlToKitListGetFiltAsync(bool lanciato, string keyRichPart, string idxMacchina, string codGruppo, DateTime startDate, DateTime endDate) diff --git a/MP.SPEC/MP.SPEC.csproj b/MP.SPEC/MP.SPEC.csproj index 8ce4107b..66b09440 100644 --- a/MP.SPEC/MP.SPEC.csproj +++ b/MP.SPEC/MP.SPEC.csproj @@ -5,7 +5,7 @@ enable enable MP.SPEC - 8.16.2605.2619 + 8.16.2605.2708 1800a78a-6ff1-40f9-b490-87fb8bfc1394 en @@ -42,6 +42,7 @@ + @@ -52,6 +53,9 @@ + + + diff --git a/MP.SPEC/Program.cs b/MP.SPEC/Program.cs index 5b8365ff..4209457e 100644 --- a/MP.SPEC/Program.cs +++ b/MP.SPEC/Program.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Authentication.Negotiate; using Microsoft.AspNetCore.StaticFiles; +using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.FileProviders; using MP.AppAuth.Services; using MP.Data.Services; @@ -12,7 +13,9 @@ using NLog.Web; using OpenTelemetry.Resources; using OpenTelemetry.Trace; using StackExchange.Redis; - +using ZiggyCreatures.Caching.Fusion; +using ZiggyCreatures.Caching.Fusion.Backplane.StackExchangeRedis; +using ZiggyCreatures.Caching.Fusion.Serialization.NewtonsoftJson; var builder = WebApplication.CreateBuilder(args); @@ -30,7 +33,7 @@ string connStringRedis = configuration.GetConnectionString("Redis") ?? "localhos //string connStringRedis = ConfMan.GetConnectionString("RedisAdmin"); string redisSrvAddr = connStringRedis.Substring(0, connStringRedis.IndexOf(":")); // avvio oggetto shared x redis... -var redisMultiplexer = ConnectionMultiplexer.Connect(connStringRedis); +IConnectionMultiplexer redisMultiplexer = ConnectionMultiplexer.Connect(connStringRedis); // ==================================================================== @@ -135,9 +138,24 @@ builder.Services.AddRazorComponents() builder.Services.AddRazorPages(); // memory + redis preliminare -builder.Services.AddMemoryCache(); builder.Services.AddSingleton(redisMultiplexer); +// ✅ Distributed cache (necessario per FusionCache) +builder.Services.AddStackExchangeRedisCache(options => +{ + options.Configuration = connStringRedis; +}); + +// ✅ FusionCache +builder.Services.AddFusionCache() + .WithDistributedCache(sp => sp.GetRequiredService()) + .WithSerializer(new FusionCacheNewtonsoftJsonSerializer()) + .WithBackplane(new RedisBackplane(new RedisBackplaneOptions + { + ConnectionMultiplexerFactory = () => Task.FromResult(redisMultiplexer) + })); + + // altri servizi builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/MP.SPEC/Resources/ChangeLog.html b/MP.SPEC/Resources/ChangeLog.html index dca5987a..59e7e6d9 100644 --- a/MP.SPEC/Resources/ChangeLog.html +++ b/MP.SPEC/Resources/ChangeLog.html @@ -1,6 +1,6 @@ Modulo MAPOSPEC -

Versione: 8.16.2605.2619

+

Versione: 8.16.2605.2708


Note di rilascio:
  • diff --git a/MP.SPEC/Resources/VersNum.txt b/MP.SPEC/Resources/VersNum.txt index 5ea9eaa9..933735f6 100644 --- a/MP.SPEC/Resources/VersNum.txt +++ b/MP.SPEC/Resources/VersNum.txt @@ -1 +1 @@ -8.16.2605.2619 +8.16.2605.2708 diff --git a/MP.SPEC/Resources/manifest.xml b/MP.SPEC/Resources/manifest.xml index b079c1c6..02e7a70d 100644 --- a/MP.SPEC/Resources/manifest.xml +++ b/MP.SPEC/Resources/manifest.xml @@ -1,6 +1,6 @@ - 8.16.2605.2619 + 8.16.2605.2708 https://nexus.steamware.net/repository/SWS/MP-SPEC/stable/LAST/MP.SPEC.zip https://nexus.steamware.net/repository/SWS/MP-SPEC/stable/LAST/ChangeLog.html false diff --git a/MP.SPEC/appsettings.json b/MP.SPEC/appsettings.json index bf3f1ae9..712e6fcb 100644 --- a/MP.SPEC/appsettings.json +++ b/MP.SPEC/appsettings.json @@ -2,7 +2,8 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Microsoft.AspNetCore": "Warning", + "ZiggyCreatures.Caching.Fusion": "Warning" } }, "NLog": {