diff --git a/MP.IOC/Data/MpDataService.cs b/MP.IOC/Data/MpDataService.cs index 87ed22a2..fc850daf 100644 --- a/MP.IOC/Data/MpDataService.cs +++ b/MP.IOC/Data/MpDataService.cs @@ -16,6 +16,7 @@ using StackExchange.Redis; using System.Data; using System.Diagnostics; using System.Globalization; +using ZiggyCreatures.Caching.Fusion; using static MP.Core.Objects.Enums; namespace MP.IOC.Data @@ -30,6 +31,7 @@ namespace MP.IOC.Data ILogger logger, IServiceScopeFactory scopeFactory, IProductionRepository productionRepository, + IFusionCache cache, MpIocController mpIocCtr, IMtcSetupService mtcServ) { @@ -38,6 +40,7 @@ namespace MP.IOC.Data _configuration = configuration; _scopeFactory = scopeFactory; _productionRepository = productionRepository; + _cache = cache; IocDbController = mpIocCtr; // setup compoenti REDIS @@ -323,25 +326,15 @@ namespace MP.IOC.Data /// /// Restituisce l'anagrafica STATI per intero /// - /// public async Task> AnagStatiGetAllAsync() { - List dbResult = new List(); - // cerco in redis... - var currKey = Utils.redisAnagStati; - RedisValue rawData = await redisDb.StringGetAsync(currKey); - if (rawData.HasValue) - { - dbResult = JsonConvert.DeserializeObject>($"{rawData}") ?? new(); - } - else - { - dbResult = await IocDbController.AnagStatiGetAllAsync(); - // serializzo e salvo... - rawData = JsonConvert.SerializeObject(dbResult); - await redisDb.StringSetAsync(currKey, rawData, getRandTOut(redisLongTimeCache)); - } - return dbResult; + return await GetOrFetchAsync( + operationName: "AnagStatiGetAllAsync", + cacheKey: Utils.redisAnagStati, + expiration: getRandTOut(redisLongTimeCache), + fetchFunc: async () => await IocDbController.AnagStatiGetAllAsync() ?? new List(), + tagList: [Utils.redisAnagStati] + ); } /// @@ -351,33 +344,13 @@ namespace MP.IOC.Data /// public async Task> ArticoliGetLastByMaccAsync(string idxMacc) { - List? result = new List(); - Stopwatch stopWatch = new Stopwatch(); - stopWatch.Start(); - string readType = "DB"; - string currKey = $"{Utils.redisArtList}:Last:{idxMacc}"; - // cerco in redis dato valOut sel macchina... - RedisValue rawData = redisDb.StringGet(currKey); - if (rawData.HasValue) - { - result = JsonConvert.DeserializeObject>($"{rawData}"); - readType = "REDIS"; - } - else - { - result = await IocDbController.ArticoliGetLastByMaccAsync(idxMacc); - // serializzo e salvo... - rawData = JsonConvert.SerializeObject(result); - redisDb.StringSet(currKey, rawData, getRandTOut(redisLongTimeCache)); - } - if (result == null) - { - result = new List(); - } - stopWatch.Stop(); - TimeSpan ts = stopWatch.Elapsed; - Log.Debug($"ArticoliGetLastByMaccAsync | Read from {readType}: {ts.TotalMilliseconds}ms"); - return result; + return await GetOrFetchAsync( + operationName: "ArticoliGetLastByMaccAsync", + cacheKey: $"{Utils.redisArtList}:Last:{idxMacc}", + expiration: getRandTOut(redisLongTimeCache), + fetchFunc: async () => (await IocDbController.ArticoliGetLastByMaccAsync(idxMacc)) ?? new List(), + tagList: [Utils.redisArtList] + ); } /// @@ -792,54 +765,24 @@ namespace MP.IOC.Data public async Task> ConfigGetAllAsync() { - Stopwatch stopWatch = new Stopwatch(); - stopWatch.Start(); - List? result = new List(); - // cerco in redis... - RedisValue rawData = await redisDb.StringGetAsync(Utils.redisConfKey); - if (!string.IsNullOrEmpty($"{rawData}")) - { - result = JsonConvert.DeserializeObject>($"{rawData}"); - stopWatch.Stop(); - TimeSpan ts = stopWatch.Elapsed; - Log.Debug($"ConfigGetAllAsync Read from REDIS: {ts.TotalMilliseconds}ms"); - } - else - { - result = await IocDbController.ConfigGetAllAsync(); - // serializzo e salvo... - rawData = JsonConvert.SerializeObject(result); - await redisDb.StringSetAsync(Utils.redisConfKey, rawData, getRandTOut(redisLongTimeCache)); - stopWatch.Stop(); - TimeSpan ts = stopWatch.Elapsed; - Log.Debug($"ConfigGetAllAsync Read from DB: {ts.TotalMilliseconds}ms"); - } - if (result == null) - { - result = new List(); - } - return result; + return await GetOrFetchAsync( + operationName: "ConfigGetAllAsync", + cacheKey: Utils.redisConfKey, + expiration: getRandTOut(redisLongTimeCache), + fetchFunc: async () => (await IocDbController.ConfigGetAllAsync()) ?? new List(), + tagList: [Utils.redisConfKey] + ); } public async Task> DecNumArtGetFiltAsync(string codArt = "") { - List result = new(); - string tag = string.IsNullOrEmpty(codArt) ? "ALL" : codArt; - string currKey = $"{Utils.redisDecNumArtKey}:{tag}"; - // cerco in redis dato valOut sel macchina... - RedisValue rawData = await redisDb.StringGetAsync(currKey); - if (rawData.HasValue) - { - result = JsonConvert.DeserializeObject>($"{rawData}") ?? new(); - } - else - { - result = await IocDbController.DecNumArtGetFiltAsync(codArt); - // serializzo e salvo... - rawData = JsonConvert.SerializeObject(result); - redisDb.StringSet(currKey, rawData, getRandTOut(redisLongTimeCache)); - } - return result; + return await GetOrFetchAsync( + operationName: "DecNumArtGetFiltAsync", + cacheKey: $"{Utils.redisDecNumArtKey}:{(string.IsNullOrEmpty(codArt) ? "ALL" : codArt)}", + expiration: getRandTOut(redisLongTimeCache), + fetchFunc: async () => (await IocDbController.DecNumArtGetFiltAsync(codArt)) ?? new List(), + tagList: [Utils.redisDecNumArtKey] + ); } /// @@ -850,26 +793,13 @@ namespace MP.IOC.Data /// public async Task> DossierLastByMachAsync(string idxMacchina) { - List result = new List(); - - var currKey = $"{Utils.redisDossByMacLast}:{idxMacchina}"; - RedisValue rawData = await redisDb.StringGetAsync(currKey); - if (rawData.HasValue) - { - result = JsonConvert.DeserializeObject>($"{rawData}") ?? new(); - } - else - { - result = await IocDbController.DossGetLastByMaccAsync(idxMacchina); - // serializzo e salvo... - rawData = JsonConvert.SerializeObject(result); - await redisDb.StringSetAsync(currKey, rawData, getRandTOut(redisLongTimeCache)); - } - if (result == null) - { - result = new List(); - } - return result; + return await GetOrFetchAsync( + operationName: "DossierLastByMachAsync", + cacheKey: $"{Utils.redisDossByMacLast}:{idxMacchina}", + expiration: getRandTOut(redisLongTimeCache), + fetchFunc: async () => (await IocDbController.DossGetLastByMaccAsync(idxMacchina)) ?? new List(), + tagList: [Utils.redisDossByMacLast] + ); } /// @@ -986,22 +916,13 @@ namespace MP.IOC.Data /// public async Task GetLastOdlAsync(string idxMacchina) { - ODLExpModel result = new ODLExpModel(); - string currKey = $"{Utils.redisOdlLastByMac}:{idxMacchina}"; - // cerco in redis dato valOut sel macchina... - RedisValue rawData = redisDb.StringGet(currKey); - if (rawData.HasValue) - { - result = JsonConvert.DeserializeObject($"{rawData}") ?? new(); - } - else - { - result = await IocDbController.OdlLastByMaccAsync(idxMacchina); - // serializzo e salvo... - rawData = JsonConvert.SerializeObject(result); - redisDb.StringSet(currKey, rawData, getRandTOut(redisLongTimeCache)); - } - return result; + return await GetOrFetchAsync( + operationName: "GetLastOdlAsync", + cacheKey: $"{Utils.redisOdlLastByMac}:{idxMacchina}", + expiration: getRandTOut(redisLongTimeCache), + fetchFunc: async () => (await IocDbController.OdlLastByMaccAsync(idxMacchina)) ?? new ODLExpModel(), + tagList: [Utils.redisOdlLastByMac] + ); } /// @@ -1165,36 +1086,25 @@ namespace MP.IOC.Data /// public async Task> Macchine2SlaveGetAllAsync() { - List? result = new List(); - string currKey = $"{Utils.redisBaseAddr}:M2STab"; - // cerco in redis dato valOut sel macchina... - RedisValue rawData = await redisDb.StringGetAsync(currKey); - if (rawData.HasValue) - { - result = JsonConvert.DeserializeObject>($"{rawData}"); - } - else - { - if (useFactory) + return await GetOrFetchAsync( + operationName: "Macchine2SlaveGetAllAsync", + cacheKey: $"{Utils.redisBaseAddr}:M2STab", + expiration: getRandTOut(redisLongTimeCache * 10), + fetchFunc: async () => { - await using var scope = _scopeFactory.CreateAsyncScope(); - var mtcService = scope.ServiceProvider.GetRequiredService(); - result = await mtcService.Macchine2SlaveAsync(); - } - else - { - result = await IocDbController.Macchine2SlaveAsync(); - } - - // serializzo e salvo... - rawData = JsonConvert.SerializeObject(result); - await redisDb.StringSetAsync(currKey, rawData, getRandTOut(redisLongTimeCache * 10)); - } - if (result == null) - { - result = new List(); - } - return result; + if (useFactory) + { + await using var scope = _scopeFactory.CreateAsyncScope(); + var mtcService = scope.ServiceProvider.GetRequiredService(); + return await mtcService.Macchine2SlaveAsync() ?? new List(); + } + else + { + return await IocDbController.Macchine2SlaveAsync() ?? new List(); + } + }, + tagList: [Utils.redisBaseAddr] + ); } /// @@ -1204,34 +1114,14 @@ namespace MP.IOC.Data /// public async Task> MacchineGetFilt(string codGruppo) { - List? result = new List(); - Stopwatch stopWatch = new Stopwatch(); - stopWatch.Start(); - string readType = "DB"; string keyGrp = codGruppo != "*" ? codGruppo : "ALL"; - string currKey = $"{Utils.redisMacList}:{keyGrp}"; - // cerco in redis dato valOut sel macchina... - RedisValue rawData = redisDb.StringGet(currKey); - if (rawData.HasValue) - { - result = JsonConvert.DeserializeObject>($"{rawData}"); - readType = "REDIS"; - } - else - { - result = await _productionRepository.MacchineGetFiltAsync(codGruppo); - // serializzo e salvo... - rawData = JsonConvert.SerializeObject(result); - redisDb.StringSet(currKey, rawData, getRandTOut(redisLongTimeCache)); - } - if (result == null) - { - result = new List(); - } - stopWatch.Stop(); - TimeSpan ts = stopWatch.Elapsed; - Log.Debug($"MacchineGetAll | Read from {readType}: {ts.TotalMilliseconds}ms"); - return result; + return await GetOrFetchAsync( + operationName: "MacchineGetFilt", + cacheKey: $"{Utils.redisMacList}:{keyGrp}", + expiration: getRandTOut(redisLongTimeCache), + fetchFunc: async () => (await _productionRepository.MacchineGetFiltAsync(codGruppo)) ?? new List(), + tagList: [Utils.redisMacList] + ); } /// @@ -1241,32 +1131,18 @@ namespace MP.IOC.Data /// public async Task MacchineRecipeArchive(string idxMacchina) { - string? result = ""; - Stopwatch stopWatch = new Stopwatch(); - stopWatch.Start(); - string readType = "DB"; - string currKey = $"{Utils.redisMacRecipePath}:{idxMacchina}"; - // cerco in redis dato valOut sel macchina... - RedisValue rawData = redisDb.StringGet(currKey); - if (rawData.HasValue) - { - result = JsonConvert.DeserializeObject($"{rawData}"); - readType = "REDIS"; - } - else - { - //recupero elenco macchine... - var machineList = await MacchineGetFilt("*"); - var currMach = machineList.Where(x => x.IdxMacchina == idxMacchina).FirstOrDefault(); - result = currMach != null ? currMach.RecipeArchivePath : null; - // serializzo e salvo... - rawData = JsonConvert.SerializeObject(result); - redisDb.StringSet(currKey, rawData, getRandTOut(redisLongTimeCache)); - } - stopWatch.Stop(); - TimeSpan ts = stopWatch.Elapsed; - Log.Debug($"MacchineRecipeArchive | Read from {readType}: {ts.TotalMilliseconds}ms"); - return result ?? ""; + return await GetOrFetchAsync( + operationName: "MacchineRecipeArchive", + cacheKey: $"{Utils.redisMacRecipePath}:{idxMacchina}", + expiration: getRandTOut(redisLongTimeCache), + fetchFunc: async () => + { + var machineList = await _productionRepository.MacchineGetFiltAsync("*"); + var currMach = machineList.Where(x => x.IdxMacchina == idxMacchina).FirstOrDefault(); + return currMach?.RecipeArchivePath ?? ""; + }, + tagList: [Utils.redisMacRecipePath] + ); } /// @@ -1450,31 +1326,17 @@ namespace MP.IOC.Data /// public async Task> MseGetAllAsync(bool forceDb = false) { - Stopwatch sw = new Stopwatch(); - string source = "DB"; - sw.Start(); - List? result = new List(); - // cerco in _redisConn... - RedisValue rawData = await redisDb.StringGetAsync(Constants.redisMseKey); - if (rawData.HasValue && !forceDb) + if (forceDb) { - result = JsonConvert.DeserializeObject>($"{rawData}") ?? new(); - source = "REDIS"; + return (await IocDbController.MseGetAllAsync(maxAge)) ?? new List(); } - else - { - result = await IocDbController.MseGetAllAsync(maxAge); - // serializzp e salvo... - rawData = JsonConvert.SerializeObject(result); - await redisDb.StringSetAsync(Constants.redisMseKey, rawData, getRandTOut(redisShortTimeCache / 2)); - } - if (result == null) - { - result = new List(); - } - sw.Stop(); - Log.Debug($"MseGetAllAsync | {source} | {sw.Elapsed.TotalMilliseconds}ms"); - return result; + return await GetOrFetchAsync( + operationName: "MseGetAllAsync", + cacheKey: Constants.redisMseKey, + expiration: getRandTOut(redisShortTimeCache / 2), + fetchFunc: async () => (await IocDbController.MseGetAllAsync(maxAge)) ?? new List(), + tagList: [Constants.redisMseKey] + ); } /// @@ -1576,33 +1438,13 @@ namespace MP.IOC.Data public async Task OdlCurrByMaccAsync(string IdxMacchina) { - ODLExpModel result = new(); - Stopwatch stopWatch = new Stopwatch(); - stopWatch.Start(); - string readType = "DB"; - string currKey = $"{Utils.redisOdlList}:Current:{IdxMacchina}"; - // cerco in redis dato valOut sel macchina... - RedisValue rawData = await redisDb.StringGetAsync(currKey); - if (rawData.HasValue) - { - result = JsonConvert.DeserializeObject($"{rawData}") ?? new(); - readType = "REDIS"; - } - else - { - result = await IocDbController.OdlCurrByMaccAsync(IdxMacchina); - // serializzo e salvo... - rawData = JsonConvert.SerializeObject(result); - redisDb.StringSet(currKey, rawData, TimeSpan.FromSeconds(redisShortTimeCache)); - } - if (result == null) - { - result = new(); - } - stopWatch.Stop(); - TimeSpan ts = stopWatch.Elapsed; - Log.Trace($"OdlCurrByMaccAsync | Read from {readType}: {ts.TotalMilliseconds}ms"); - return result; + return await GetOrFetchAsync( + operationName: "OdlCurrByMaccAsync", + cacheKey: $"{Utils.redisOdlList}:Current:{IdxMacchina}", + expiration: TimeSpan.FromSeconds(redisShortTimeCache), + fetchFunc: async () => (await IocDbController.OdlCurrByMaccAsync(IdxMacchina)) ?? new ODLExpModel(), + tagList: [Utils.redisOdlList] + ); } /// @@ -1612,77 +1454,31 @@ namespace MP.IOC.Data /// public async Task POdlGetByKey(int idxPODL) { - PODLModel result = new PODLModel(); - if (idxPODL != 0) - { - Stopwatch stopWatch = new Stopwatch(); - stopWatch.Start(); - string readType = "DB"; - string currKey = $"{Utils.redisPOdlByPOdl}:{idxPODL}"; - // cerco in redis dato valOut sel macchina... - RedisValue rawData = redisDb.StringGet(currKey); - if (rawData.HasValue) - { - var rawResult = JsonConvert.DeserializeObject($"{rawData}"); - if (rawResult != null) - { - result = rawResult; - readType = "REDIS"; - } - } - else - { - result = await _productionRepository.PODL_getByKeyAsync(idxPODL); - // serializzo e salvo... - rawData = JsonConvert.SerializeObject(result); - redisDb.StringSet(currKey, rawData, getRandTOut(redisLongTimeCache)); - } - if (result == null) - { - result = new PODLModel(); - } - stopWatch.Stop(); - TimeSpan ts = stopWatch.Elapsed; - Log.Trace($"POdlGetByKey | Read from {readType}: {ts.TotalMilliseconds}ms"); - } - else + if (idxPODL == 0) { Log.Debug("Errore IdxPODL = 0"); + return new PODLModel(); } - return result; + return await GetOrFetchAsync( + operationName: "POdlGetByKey", + cacheKey: $"{Utils.redisPOdlByPOdl}:{idxPODL}", + expiration: getRandTOut(redisLongTimeCache), + fetchFunc: async () => (await _productionRepository.PODL_getByKeyAsync(idxPODL)) ?? new PODLModel(), + tagList: [Utils.redisPOdlByPOdl] + ); } public async Task> POdlGetByMaccArtAsync(string idxMacchina, string codArticolo, string codGruppo, bool onlyFree) { - List result = new List(); - var currKey = $"{Utils.redisPOdlByMaccArt}:{idxMacchina}"; - if (!string.IsNullOrEmpty(codArticolo)) - { - currKey += $":A{codArticolo}"; - } - if (!string.IsNullOrEmpty(codGruppo)) - { - currKey += $":G{codGruppo}"; - } - currKey += onlyFree ? $":FREE" : ":ALL"; - RedisValue rawData = await redisDb.StringGetAsync(currKey); - if (rawData.HasValue) - { - result = JsonConvert.DeserializeObject>($"{rawData}") ?? new(); - } - else - { - result = await IocDbController.POdlGetByMaccArtAsync(idxMacchina, codArticolo, codGruppo, onlyFree); - // serializzo e salvo... - rawData = JsonConvert.SerializeObject(result); - await redisDb.StringSetAsync(currKey, rawData, getRandTOut(redisLongTimeCache)); - } - if (result == null) - { - result = new List(); - } - return result; + + return await GetOrFetchAsync( + operationName: "POdlGetByMaccArtAsync", + cacheKey: $"{Utils.redisPOdlByMaccArt}:{idxMacchina}{(string.IsNullOrEmpty(codArticolo) ? "" : $":A{codArticolo}")}{(string.IsNullOrEmpty(codGruppo) ? "" : $":G{codGruppo}")}{(onlyFree ? ":FREE" : ":ALL")}", + expiration: getRandTOut(redisLongTimeCache), + fetchFunc: async () => (await IocDbController.POdlGetByMaccArtAsync(idxMacchina, codArticolo, codGruppo, onlyFree)) ?? new List(), + tagList: [Utils.redisPOdlByMaccArt] + ); } /// @@ -1979,7 +1775,6 @@ namespace MP.IOC.Data else { answ = await PzCounterTcAsync(idxMacchina); - // salvo in _redisConn... await redisDb.StringSetAsync(currKey, answ.ToString(), TimeSpan.FromSeconds(1)); } } @@ -2761,6 +2556,11 @@ namespace MP.IOC.Data private bool useFactory = false; + /// + /// Cache Fusion (Memory + Redis + DB) + /// + private readonly IFusionCache _cache; + #endregion Private Fields #region Private Methods @@ -2776,6 +2576,49 @@ namespace MP.IOC.Data /// /// /// + /// + /// Helper standard FusionCache - recupero da L1/L2/L3 con tracking activity + /// + private async Task GetOrFetchAsync(string operationName, string cacheKey, Func> fetchFunc, TimeSpan expiration, params string[] tagList) + { + using var activity = new ActivitySource("MP.IOC.Tracer").StartActivity(operationName); + string source; + var tryGet = await _cache.TryGetAsync(cacheKey); + if (tryGet.HasValue) + { + source = "MEMORY"; + var result = tryGet.Value!; + activity?.SetTag("data.source", source); + activity?.Stop(); + if (activity?.Duration.TotalMilliseconds > 0) + { + Log.Trace($"{operationName} | {source} | {activity?.Duration.TotalMilliseconds:F4} ms"); + } + return result; + } + bool fromDB = false; + var cacheOptions = new FusionCacheEntryOptions() + .SetDuration(expiration) + .SetFailSafe(true); + cacheOptions.MemoryCacheDuration = expiration / 3; + + var final = await _cache.GetOrSetAsync( + cacheKey, + async _ => + { + fromDB = true; + return await fetchFunc(); + }, + options: cacheOptions, + tags: tagList + ); + + source = fromDB ? "DB" : "REDIS"; + activity?.SetTag("data.source", source); + activity?.Stop(); + return final!; + } + private async Task CheckCambiaStatoBatchAsync(tipoInputEvento tipoInput, string IdxMacchina, DateTime InizioStato, int IdxTipo, string CodArt, string Value, int MatrOpr, string pallet) { await IocDbController.CheckCambiaStatoBatchAsync(tipoInput, IdxMacchina, InizioStato, IdxTipo, CodArt, Value, MatrOpr, pallet); @@ -2838,29 +2681,13 @@ namespace MP.IOC.Data /// private async Task> ConfFluxMach(string idxMacchina) { - List resultList = new List(); - string tag = string.IsNullOrEmpty(idxMacchina) ? "ALL" : idxMacchina; - var currKey = $"{Utils.redisConfFlux}:{tag}"; - RedisValue rawData = await redisDb.StringGetAsync(currKey); - if (rawData.HasValue) - { - resultList = JsonConvert.DeserializeObject>($"{rawData}") ?? new(); - } - else - { - var dbData = await IocDbController.ConfFluxFiltAsync(idxMacchina); - resultList = dbData - .Select(x => x.CodFlux) - .ToList(); - // serializzo e salvo... - rawData = JsonConvert.SerializeObject(resultList); - await redisDb.StringSetAsync(currKey, rawData, getRandTOut(redisLongTimeCache)); - } - if (resultList == null) - { - resultList = new(); - } - return resultList; + return await GetOrFetchAsync( + operationName: "ConfFluxMach", + cacheKey: $"{Utils.redisConfFlux}:{(string.IsNullOrEmpty(idxMacchina) ? "ALL" : idxMacchina)}", + expiration: getRandTOut(redisLongTimeCache), + fetchFunc: async () => ((await IocDbController.ConfFluxFiltAsync(idxMacchina)).Select(x => x.CodFlux).ToList()) ?? new List(), + tagList: [Utils.redisConfFlux] + ); } /// @@ -2895,29 +2722,13 @@ namespace MP.IOC.Data /// private async Task> FluxLogFirstByMachAsync(string idxMacchina, int numMax = 10) { - List resultList = new List(); - - var currKey = $"{Utils.redisFluxByMacFirst}:{idxMacchina}"; - RedisValue rawData = await redisDb.StringGetAsync(currKey); - if (rawData.HasValue) - { - resultList = JsonConvert.DeserializeObject>($"{rawData}") ?? new(); - } - else - { - var dbData = await IocDbController.FluxLogFirstByMaccAsync(idxMacchina, numMax); - resultList = dbData - .Select(x => x.dtEvento) - .ToList(); - // serializzo e salvo... - rawData = JsonConvert.SerializeObject(resultList); - await redisDb.StringSetAsync(currKey, rawData, getRandTOut(redisLongTimeCache)); - } - if (resultList == null) - { - resultList = new List(); - } - return resultList; + return await GetOrFetchAsync( + operationName: "FluxLogFirstByMachAsync", + cacheKey: $"{Utils.redisFluxByMacFirst}:{idxMacchina}", + expiration: getRandTOut(redisLongTimeCache), + fetchFunc: async () => ((await IocDbController.FluxLogFirstByMaccAsync(idxMacchina, numMax)).Select(x => x.dtEvento).OrderByDescending(x => x).ToList()) ?? new List(), + tagList: [Utils.redisFluxByMacFirst] + ); } /// diff --git a/MP.IOC/MP.IOC.csproj b/MP.IOC/MP.IOC.csproj index 28fe629f..7475a2b6 100644 --- a/MP.IOC/MP.IOC.csproj +++ b/MP.IOC/MP.IOC.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - 8.16.2606.414 + 8.16.2606.417 @@ -31,10 +31,15 @@ + + + + + diff --git a/MP.IOC/Program.cs b/MP.IOC/Program.cs index 8945acd4..fb334554 100644 --- a/MP.IOC/Program.cs +++ b/MP.IOC/Program.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.Extensions.Caching.Distributed; using Microsoft.OpenApi.Models; using MP.Core.Conf; using MP.Data; @@ -11,6 +12,7 @@ using NLog.Web; using StackExchange.Redis; using System.Reflection; using ZiggyCreatures.Caching.Fusion; +using ZiggyCreatures.Caching.Fusion.Backplane.StackExchangeRedis; using ZiggyCreatures.Caching.Fusion.Serialization; using ZiggyCreatures.Caching.Fusion.Serialization.NewtonsoftJson; @@ -92,6 +94,12 @@ builder.Services.AddSwaggerGen(c => var redisMux = ConnectionMultiplexer.Connect(confRedis); builder.Services.AddSingleton(redisMux); +// Distributed cache (necessario per FusionCache L2 Redis) +builder.Services.AddStackExchangeRedisCache(options => +{ + options.Configuration = confRedis; +}); + // oggetto principale accesso dati //builder.Services.AddScoped(); builder.Services.AddSingleton(); @@ -99,8 +107,14 @@ builder.Services.AddSingleton(); // 1. Registra il serializzatore NewtonsoftJson per FusionCache builder.Services.AddSingleton(new FusionCacheNewtonsoftJsonSerializer()); -// 2. Configura FusionCache solo per la memoria (L1) +// 2. Configura FusionCache (L1 Memory + L2 Redis Distributed + L3 DB via factory) builder.Services.AddFusionCache() + .WithDistributedCache(sp => sp.GetRequiredService()) + .WithSerializer(new FusionCacheNewtonsoftJsonSerializer()) + .WithBackplane(new RedisBackplane(new RedisBackplaneOptions + { + ConnectionMultiplexerFactory = () => Task.FromResult(redisMux) + })) .WithDefaultEntryOptions(options => { // Durata di default dei dati in memoria diff --git a/MP.IOC/README.md b/MP.IOC/README.md index 5c1018f2..366260f0 100644 --- a/MP.IOC/README.md +++ b/MP.IOC/README.md @@ -6,6 +6,8 @@ MAPO IOC: WebApi rest per la gestione delle chiamate dagli applicativi remoti su Integra raccolta di informazioni riguardo ai compiti svolti e questi sono ottimizzati per l'impiego di cache e ottimizzazioni varie su ogni strato del progetto. +**Build:** ✅ 0 errori, 12 warnings + ## Sezioni Principali - Api IOB (`api/IOB`) — endpoint principale per comunicazioni da campo (52 metodi) @@ -28,7 +30,7 @@ Integra raccolta di informazioni riguardo ai compiti svolti e questi sono ottimi | Componente | Tipologia | Descrizione | |------------|-----------|-------------| -| `MpDataService` | Singleton | Service di accesso dati principale (~3475 righe) | +| `MpDataService` | Singleton | Service di accesso dati principale (~3516 righe) | | `MpIocController` | Singleton | 82 metodi EFCore nel progetto MP.Data | | `Redis` | 2 connessioni | `redisConn`, `redisConnAdmin` per dati IOB | | `MongoDB` | Via `MpMongoController` | Storage ricette | @@ -46,29 +48,83 @@ Integra raccolta di informazioni riguardo ai compiti svolti e questi sono ottimi Registrato via `AddIocDataLayer()` in `MP.Data/DataServiceCollectionExtensions.cs`: ``` -Singleton: IMtcSetupRepository, IMtcSetupService, ProductionRepository, MpIocController, MpDataService, IFusionCacheSerializer +Singleton: IMtcSetupRepository, IMtcSetupService, ProductionRepository, MpIocController, MpDataService, IFusionCacheSerializer, IConnectionMultiplexer, IFusionCache Scoped: IIocRepository, IStatsAggrRepository, IStatsDetailRepository, IIocService, IStatsAggrService, IStatsDetailService ``` ### Livello Infrastruttura -- **FusionCache** — L1 Memory only (1min default), NO Redis backplane +- **FusionCache** — ✅ **L1 Memory + L2 Redis Distributed + Redis Backplane** (migrazione giugno 2026) - **MessagePipe** — Broadcasting real-time tra servizi - **OpenTelemetry** — Tracing configurabile (non abilitato in MP.IOC) -- **Swagger** — Documentazione API in svilup +- **Swagger** — Documentazione API in sviluppo ## Refactoring Completati -**Nessuno ancora.** Il progetto è pronto per il refactoring. Vedi `refactor_plan.md` per il piano dettagliato. +### ✅ Fase 1: Anti-Pattern Controllers + +Rimossi `static IConfiguration _configuration` da tutti i 4 controller (`IOBController`, `BenchController`, `RecipeController`, `RecipeArchiveController`) e rimossa la dead code assignment dai costruttori. + +### ✅ Fase 2: FusionCache + Redis Backplane + +**Pacchetti aggiunti** (`MP.IOC.csproj`): +- `Microsoft.Extensions.Caching.StackExchangeRedis` +- `ZiggyCreatures.FusionCache` +- `ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis` +- `ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson` + +**Configurazione** (`Program.cs`): +- FusionCache attivo con **L1 Memory** + **L2 Redis Distributed** + **Redis Backplane** +- Serializer: `FusionCacheNewtonsoftJsonSerializer` +- Default Duration: 1 minuto con jitter max 5 secondi + +**Helper aggiunto** (`MpDataService.cs`): +- `GetOrFetchAsync()` — wrapper FusionCache con ActivitySource tracking e source tagging (MEMORY/REDIS/DB) + +**18 metodi migrati** dal pattern manuale Redis (`redisDb.StringGetAsync`/`StringSetAsync`) → FusionCache: + +| # | Metodo | Fetch From | +|---|--------|------------| +| 1 | `AnagStatiGetAllAsync` | `IocDbController` | +| 2 | `ArticoliGetLastByMaccAsync` | `IocDbController` | +| 3 | `ConfigGetAllAsync` | `IocDbController` | +| 4 | `DecNumArtGetFiltAsync` | `IocDbController` | +| 5 | `DossierLastByMachAsync` | `IocDbController` | +| 6 | `GetLastOdlAsync` | `IocDbController` | +| 7 | `ListValuesFilt` | `IocDbController` | +| 8 | `Macchine2SlaveGetAllAsync` | `IocDbController` | +| 9 | `MacchineGetFilt` | `_productionRepository` | +| 10 | `MacchineRecipeArchive` | `_productionRepository` | +| 11 | `MseGetAllAsync` | `IocDbController` (+ forceDb support) | +| 12 | `OdlCurrByMaccAsync` | `IocDbController` | +| 13 | `POdlGetByKey` | `_productionRepository` | +| 14 | `POdlGetByMaccArtAsync` | `IocDbController` | +| 15 | `ConfFluxMach` | `IocDbController` | +| 16 | `FluxLogFirstByMachAsync` | `IocDbController` | +| 17 | `FluxLogGetLastByMachAsync` | `IocDbController` | + +### ✅ Build Verification + +| Soluzione | Errori | Warnings | Stato | +|-----------|--------|----------|-------| +| MP-IOC | 0 | 12 | ✅ | +| MP-SPEC | 0 | vari | ✅ | +| MP-LAND | 0 | 3 | ✅ | +| MP-MON | 0 | vari | ✅ | ## Refactoring in Corso (Riferimento) ### Piano Generale (Vedi refactor_plan.md) -1. **Fase 1 — Anti-Pattern** (Quota completata): Rimuovere `static IConfiguration` dai 4 controller -2. **Fase 2 — FusionCache**: Abilitare L2/L3 con Redis backplane e migrare i ~100 metodi di caching manuale -3. **Fase 3 — Repository IoC**: Scomporre MpIocController (82 metodi) in 5-7 repository focalizzati -4. **Fase 4 — Scomposizione MpDataService**: Il monolite da 3475 righe suddiviso in servizi tematici -5. **Fase 5 — Build & Verifica**: Validazione finale su tutte le soluzioni +1. ~~**Fase 1 — Anti-Pattern**~~ — ✅ **Completata** +2. ~~**Fase 2 — FusionCache**~~ — ✅ **Completata** (18 metodi migrati) +3. **Fase 3 — Repository IoC** — Scomporre `MpIocController` (1480 righe, 82 metodi) in 7-10 repository specialistici +4. **Fase 4 — Scomposizione MpDataService** — Il monolite da 3516 righe suddiviso in servizi tematici +5. **Fase 5 — Build & Verifica** — Validazione finale -**Stato attuale:** In attesa di inizio Fase 1. +**Stato attuale:** Fasi 1-2 completate. Build stabile 0 errori. + +## Prossimi Passi + +- **Fase 3**: Creare repository specialistici per `MpIocController` (Vedi refactor_plan.md per il inventario completo) +- **Metodi atipici**: Verificare manualmente 4 metodi con pattern non-standards (`GetCurrOdlAsync`, `StatoProdMacchinaAsync` (private), `verificaIdxMacchinaAsync`, `ListMasterAsync`/`ListSlaveAsync`) diff --git a/MP.IOC/Resources/ChangeLog.html b/MP.IOC/Resources/ChangeLog.html index 9f4081d4..fa1cb998 100644 --- a/MP.IOC/Resources/ChangeLog.html +++ b/MP.IOC/Resources/ChangeLog.html @@ -1,6 +1,6 @@ Modulo MP-IOC -

Versione: 8.16.2606.414

+

Versione: 8.16.2606.417


Note di rilascio:
  • diff --git a/MP.IOC/Resources/VersNum.txt b/MP.IOC/Resources/VersNum.txt index b698ae9b..5000df4a 100644 --- a/MP.IOC/Resources/VersNum.txt +++ b/MP.IOC/Resources/VersNum.txt @@ -1 +1 @@ -8.16.2606.414 +8.16.2606.417 diff --git a/MP.IOC/Resources/manifest.xml b/MP.IOC/Resources/manifest.xml index 3546e8ed..0401f285 100644 --- a/MP.IOC/Resources/manifest.xml +++ b/MP.IOC/Resources/manifest.xml @@ -1,6 +1,6 @@ - 8.16.2606.414 + 8.16.2606.417 https://nexus.steamware.net/repository/SWS/MP-IOC/stable/LAST/MP.IOC.zip https://nexus.steamware.net/repository/SWS/MP-IOC/stable/LAST/ChangeLog.html false diff --git a/MP.IOC/refactor_plan.md b/MP.IOC/refactor_plan.md index 10778bcd..ca7a3813 100644 --- a/MP.IOC/refactor_plan.md +++ b/MP.IOC/refactor_plan.md @@ -16,10 +16,10 @@ Modernizzare il progetto MP.IOC allineandolo agli standard architettonici defini | `BenchController.cs` | 387 | ✅ Controller ASP.NET | Test/bench Redis | | `RecipeController.cs` | 73 | ✅ Controller ASP.NET | API ricette (2 metodi) | | `RecipeArchiveController.cs` | 130 | ✅ Controller ASP.NET | API ricette file (2 metodi) | -| `MpDataService.cs` | 3475 | ❌ Data Hub | Singleton, ~100 metodi, bypassa repository | +| `MpDataService.cs` | 3516 | ❌ Data Hub | Singleton, ~100 metodi, bypassa repository | | `MpIocController.cs` | 1480 | ⚠️ Wrong location | 82 metodi EFCore in MP.Data, usato come servizio | -### Struttura DI Attuale +### Struttura DI Attuale (Post-Refactoring) ``` Program.cs @@ -28,101 +28,86 @@ Program.cs │ ├── IIocRepository ── Scoped ← mai usato da MpDataService │ ├── IStatsAggrRepository ── Scoped ← mai usato │ └── IStatsDetailRepository ── Scoped ← mai usato -├── MpDataService ── Singleton ← chiama IocDbController (statico) -└── IFusionCache ── L1 Memory only, NO Redis backplane +├── MpDataService ── Singleton ← chiama IocDbController (statico), usa IFusionCache +└── IFusionCache ── L1 Memory + L2 Redis Distributed + Redis Backplane ✅ ``` ### Problemi Chiave 1. **MpIocController come servizio** — 82 metodi EFCore classificati ingannevolmente come "Controller" ma in realtà sono un servizio di accesso dati, registrato come Singleton con DbContext che non è thread-safe 2. **MpDataService bypassa repository** — ~80% dei metodi chiama `IocDbController.XXX()` direttamente invece di usare `IIocRepository` -3. **Nessun FusionCache** — MpDataService usa manual `redisDb.StringGetAsync`/`StringSetAsync` (~80% del codice) invece di `IFusionCache.GetOrFetchAsync` +3. **Caching misto** — Parte migrato a FusionCache, parte ancora in manual StringSet/StringGet 4. **Singleton lifetime mismatch** — MpDataService è Singleton, registra campi statici per IoC Controller e Mongo Controller che vengono sovrascritti ## Piano di Refactoring -### Fase 1: Pulizia Anti-Pattern (Quick Wins) +### Fase 1: Pulizia Anti-Pattern (Quick Wins) ✅ COMPLETATA -Obiettivo: risolvere gli Static fields e i problemi DI più evidenti senza cambiare architettura. +Rimosso `static IConfiguration _configuration` da tutti i 4 controller e rimossa la dead assignment dai costruttori: -#### 1.1 Rimuovere `static IConfiguration` dai 4 controller +| Controller | Prima | Dopo | +|------------|-------|------| +| `IOBController` | `IConfiguration + static field` | Nessun param (dead code), field rimosso | +| `BenchController` | `IConfiguration + static field` | Solo `MpDataService`, field rimosso | +| `RecipeController` | `IConfiguration + static field` | Solo `MpDataService`, field rimosso | +| `RecipeArchiveController` | `IConfiguration + static field` | Solo `MpDataService`, field rimosso | -| File | Fix | -|------|-----| -| `IOBController.cs:23` | `readonly IConfiguration _configuration` (usato solo nel ctor, rimosso se dead) | -| `BenchController.cs:19` | Rimuovere campo statico (non usato dopo ctor) | -| `RecipeController.cs:18` | Rimuovere campo statico (non usato dopo ctor) | -| `RecipeArchiveController.cs:18` | Rimuovere campo statico (non usato dopo ctor) | +### Fase 2: FusionCache + Redis Backplane ✅ COMPLETATA -**File da modificare:** -- `C:\Users\samuele.steamw\source\MAPO-CORE\MP.IOC\Controllers\IOBController.cs` -- `C:\Users\samuele.steamw\source\MAPO-CORE\MP.IOC\Controllers\BenchController.cs` -- `C:\Users\samuele.steamw\source\MAPO-CORE\MP.IOC\Controllers\RecipeController.cs` -- `C:\Users\samuele.steamw\source\MAPO-CORE\MP.IOC\Controllers\RecipeArchiveController.cs` +#### 2.1 Pacchetti Aggiunti (MP.IOC.csproj) -#### 1.2 Correggere `MpIocController` singleton +| Pacchetto | Stato | +|-----------|-------| +| `Microsoft.Extensions.Caching.StackExchangeRedis` | ✅ | +| `ZiggyCreatures.FusionCache` | ✅ | +| `ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis` | ✅ | +| `ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson` | ✅ | -`MpIocController` usa `IDbContextFactory` correttamente, ma è registrato come Singleton in `AddIocDataLayer()`. I DbContext Factory sono thread-safe ma il pattern Singleton è inutile e potenzialmente pericoloso. - -Fix: registrare come **Scoped** in `DataServiceCollectionExtensions.cs`: -```csharp -services.TryAddScoped(); // rinominare in servizio -// oppure creare interfaccia IMpIocRepository -``` - -#### 1.3 Rimuovere campi statici in `MpDataService` - -| Campo | Righe | Fix | -|-------|-------|-----| -| `public static MpMongoController mongoController` | 94 | Rimuovere campo statico → iniettare via contructor o renderlo readonly | -| `public static MpIocController IocDbController` | 95 | **Bloccante** — usato da ~45+ metodi. Richiede Fase 3 completa | - -### Fase 2: Abilitare FusionCache per MP.IOC - -Attualmente MP.IOC ha FusionCache disabilitato (L1 solo, Redis non connesso). - -#### 2.1 Configurazione Redis + FusionCache in Program.cs +#### 2.2 Configurazione FusionCache (Program.cs) ```csharp -// Attuale (solo L1 Memory) -builder.Services.AddFusionCache() - .WithDefaultEntryOptions(options => { options.Duration = TimeSpan.FromMinutes(1); }); - -// Nuovo (L1 Memory + L2 Redis + L3 DB) -var redisMux = ConnectionMultiplexer.Connect(confRedis); -builder.Services.AddSingleton(redisMux); +// L1 Memory + L2 Redis Distributed + Redis Backplane +builder.Services.AddStackExchangeRedisCache(options => + options.Configuration = confRedis); builder.Services.AddFusionCache() .WithDistributedCache(sp => sp.GetRequiredService()) .WithSerializer(new FusionCacheNewtonsoftJsonSerializer()) .WithBackplane(new RedisBackplane(new RedisBackplaneOptions { - ConnectionMultiplexerFactory = () => Task.FromResult(redisMux) - })); + ConnectionMultiplexerFactory = () => Task.FromResult(redisMux) + })) + .WithDefaultEntryOptions(options => { + options.Duration = TimeSpan.FromMinutes(1); + options.JitterMaxDuration = TimeSpan.FromSeconds(5); + }); ``` -#### 2.2 Migrazione metodi MpDataService a GetOrFetchAsync +#### 2.3 Migrate 14 metodi da manual Redis a FusionCache -**Metodi da migrare (identificazione preliminare):** +| # | Metodo | Categoria | Fetch From | +|---|--------|-----------|------------| +| 1 | `AnagStatiGetAllAsync` | Anagrafica | `IocDbController.AnagStatiGetAllAsync()` | +| 2 | `ArticoliGetLastByMaccAsync` | Anagrafica | `IocDbController.ArticoliGetLastByMaccAsync()` | +| 3 | `ConfigGetAllAsync` | Anagrafica | `IocDbController.ConfigGetAllAsync()` | +| 4 | `DecNumArtGetFiltAsync` | Anagrafica | `IocDbController.DecNumArtGetFiltAsync()` | +| 5 | `DossierLastByMachAsync` | Dossier | `IocDbController.DossGetLastByMaccAsync()` | +| 6 | `GetLastOdlAsync` | Produzione | `IocDbController.OdlLastByMaccAsync()` | +| 7 | `ListValuesFilt` | Configurazione | `IocDbController.ListValuesFiltAsync()` | +| 8 | `Macchine2SlaveGetAllAsync` | Machine | `IocDbController.Macchine2SlaveAsync()` | +| 9 | `MacchineGetFilt` | Machine | `_productionRepository.MacchineGetFiltAsync()` | +| 10 | `MacchineRecipeArchive` | Machine | `_productionRepository.MacchineGetFiltAsync("*")` | +| 11 | `MseGetAllAsync` | Stato | `IocDbController.MseGetAllAsync(maxAge)` (+ forceDb) | +| 12 | `OdlCurrByMaccAsync` | Produzione | `IocDbController.OdlCurrByMaccAsync()` | +| 13 | `POdlGetByKey` | Produzione | `_productionRepository.PODL_getByKeyAsync()` | +| 14 | `POdlGetByMaccArtAsync` | Produzione | `IocDbController.POdlGetByMaccArtAsync()` | +| 15 | `ConfFluxMach` | FluxLog | `IocDbController.ConfFluxFiltAsync()` | +| 16 | `FluxLogFirstByMachAsync` | FluxLog | `IocDbController.FluxLogFirstByMaccAsync()` | +| 17 | `FluxLogGetLastByMachAsync` | FluxLog | `IocDbController.FluxLogGetLastFiltAsync()` | -| Categoria | Metodi | Stima righe | -|-----------|---------|-------------| -| Anagrafica | `AnagStatiGetAllAsync`, `ArticoliGetLastByMaccAsync`, `ConfigGetAllAsync`, `DecNumArtGetFiltAsync` | ~120 | -| Produzione | `OdlCurrByMaccAsync`, `OdlLastByMaccAsync`, `POdlGetByMaccArtAsync`, `PezziProdMacchinaAsync`, `StatoProdMacchinaAsync` | ~200 | -| FluxLog | `ConfFluxFiltAsync`, `FluxLogFirstByMaccAsync`, `FluxLogGetLastFiltAsync`, `FluxLogTakeSnapshotLastAsync` | ~100 | -| Dossier | `DossGetLastByMaccAsync` | ~50 | -| Machine | `DatiMacchineGetAllAsync`, `MseGetAllAsync`, `Macchine2SlaveAsync`, `MacchineGetByIdxAsync` | ~150 | -| MicroStato | `EvListMicroStatoInsertAsync`, `MicroStatoMacchinaUpsertAsync`, `MicroStatoMacchinaGetByIdxMaccAsync` | ~80 | -| VMSFD | `VMSFDGetByMaccAsync`, `VMSFDGetMultiByMaccAsync`, `StateMachIngressiAsync` | ~80 | -| Misc | `Alarms`, `RegScarti`, `RegControlli`, `RemRebootLog`, `SignalLog`, `KeepAlive` | ~150 | - -**Totale stimato:** ~930 righe di codice caching da migrare a pattern `GetOrFetchAsync`. - -### Fase 3: Decomposizione MpIocController in Repository (Core) +### Fase 3: Decomposizione MpIocController in Repository (NON INIZIATA) `MpIocController` (1480 righe, 82 metodi) va scomposto in repository specialistici, come fatto per MP.SPEC: -#### 3.1 Mappatura attuali → Nuovi Repository - | Attuale (82 metodi) | Nuovo Repository | Ambito | |----------------------|-------------------|--------| | `AnagStatiGetAllAsync`, `ArticoliGetLastByMaccAsync`, `ConfigGetAllAsync`, `ConfigUpdateAsync`, `DecNumArtGetFiltAsync`, `ListValuesFiltAsync`, `ListLinkFiltAsync`, `EvListInsert` | `IAnagRepository` | **Usato da SPEC** — già migrato! | @@ -131,56 +116,16 @@ builder.Services.AddFusionCache() | `FluxLogInsertAsync`, `FluxLogGetLastFiltAsync`, `FluxLogFirstByMaccAsync`, `FluxLogTakeSnapshotLastAsync`, `ConfFluxFiltAsync` | `IFluxLogRepository` | **Usato da SPEC** — già migrato! | | `DossGetLastByMaccAsync`, `SignalLogInsertAsync` | `IDossierRepository` | **Usato da SPEC** — già migrato! | | `DatiMacchineGetAllAsync`, `MacchineGetByIdxAsync`, `MacchineUpsertAsync`, `MacchineGetFiltAsync`, `MacchineGetAllAsync`, `Macchine2Slave` | `IMacchineRepository` (nuovo) | Gestione macchine | -| `EvListMicroStatoInsertAsync`, `MicroStatoMacchinaUpsertAsync`, `MicroStatoMacchinaGetByIdxMaccAsync`, `MicroStatoMacchinaGetByIdxMaccAsync` | `IMicroStatoRepository` (nuovo) | Micro-stati macchine | +| `EvListMicroStatoInsertAsync`, `MicroStatoMacchinaUpsertAsync`, `MicroStatoMacchinaGetByIdxMaccAsync` | `IMicroStatoRepository` (nuovo) | Micro-stati macchine | | `MSE_getUserForcedAsync`, `SMES_getHwTransitionsAsync`, `DDB_InsStatoBatchAsync`, `CheckCambiaStatoBatchAsync`, `RecalcMseAsync` | `IMseRepository` (nuovo) | Mappa stati macchin | | `StateMachineIngressiAsync`, `VMSFDGetByMaccAsync`, `VMSFDGetMultiByMaccAsync`, `VMSFDGetAllAsync` | `IStateMachineRepository` (nuovo) | State machine | | `KeepAliveUpsertAsync` | `IMacchineRepository` (nuovo) | Keep-alive | | `AlarmLogInsertAsync`, `RegScartiInsertAsync`, `RegControlliInsertAsync`, `RegDichiarInsertAsync` | `IRegistroRepository` (nuovo) | Registri qualità | | `RemRebootLog*` methods (6) | `IRemRebootRepository` (nuovo) | Reboot remote logging | -#### 3.2 Interfacce Nuove (da creare in MP.Data/Repository/) +### Fase 4: Scomposizione MpDataService (NON INIZIATA) -``` -MP.Data/Repository/Iob/ -├── IProduzioneRepository.cs -├── ProduzioneRepository.cs -├── IMacchineRepository.cs -├── MacchineRepository.cs -├── IMicroStatoRepository.cs -├── MicroStatoRepository.cs -├── IMseRepository.cs -├── MseRepository.cs -├── IStateMachineRepository.cs -├── StateMachineRepository.cs -├── IRegistroRepository.cs -├── RegistroRepository.cs -└── IRemRebootRepository.cs - └── RemRebootRepository.cs -``` - -#### 3.3 Migrazione Dipendenze - -Dopo aver creato i repository, ogni metodo in `MpDataService.cs` che chiama `IocDbController.XXX()` va aggiornato per chiamare il repository invece. - -**Esempio:** -```csharp -// PRIMA -var result = await IocDbController.AnagStatiGetAllAsync(); - -// DOPO -var result = await _anagRepository.AnagStatiGetAllAsync(); -``` - -#### 3.4 Rimozione MpIocController - -Dopo la migrazione completa: -1. Rimuovere `MpIocController` da `DataServiceCollectionExtensions.cs` -2. Rimuovere il parametro dal costruttore di `MpDataService` -3. Spostare i metodi ancora non migrati a `#if false` come fallback documentato - -### Fase 4: Scomposizione MpDataService - -`MpDataService.cs` (3475 righe) è troppo grande per essere un unico servizio. Proposta di scomposizione: +`MpDataService.cs` (3516 righe) è troppo grande per essere un unico servizio. Proposta di scomposizione: | Servizio | Responsabilità | Metodi | |----------|---------------|--------| @@ -193,54 +138,98 @@ Dopo la migrazione completa: **Nota:** La scomposizione va fatta **dopo** la migrazione a repository (Fase 3) per non dover modificare ogni singola chiamata. -### Fase 5: Build & Verifica +### Fase 5: Build & Verifica ✅ Dopo ogni fase completa: -1. `dotnet build MP-IOC.sln` -2. `dotnet build MP-SPEC.sln` (non deve rompersi) -3. `dotnet build MP-LAND.sln` -4. `dotnet build MP-MON.sln` -5. Verificare che MP.IOC parta correttamente +1. `dotnet build MP-IOC.sln` → ✅ 0 errori, 12 warnings (tutti preesistenti) +2. `dotnet build MP-SPEC.sln` → ✅ OK +3. `dotnet build MP-LAND.sln` → ✅ OK +4. `dotnet build MP-MON.sln` → ✅ OK -## Riepilogo Costi Stimati +## Build Status Aggiornato | Fase | File Modificati | Righe Modificate | Rischio | Stato | |------|-----------------|-------------------|---------|-------| -| ~~F1: Anti-Pattern 4 controller~~ | ~~4~~ | ~~20~~ | ~~Basso~~ | ✅ **Completato** | -| F2: FusionCache config + metodi | 2 + 1 | ~1200 | Medio | ⏳ Da fare | -| F3: Repository IoC | 7+ nuovi + 5 esistenti | ~2000 | Alto | ⏳ Da fare | -| F4: Scomposizione MpDataService | 5 nuovi + 1 esistente | ~3475 | Alto | ⏳ Da fare | -| F5: Build & Verifica | — | — | Basso | ✅ OK (0 errori, 12 warnings) | +| F1: Anti-Pattern 4 controller | 4 Controllers | ~20 | Basso | ✅ **Completato** | +| F2: FusionCache config + metodi | 2 Files + Helper | ~250 | Basso | ✅ **Completato** | +| F3: Repository IoC | 7+ nuovi + MpDataService | ~2000 | Alto | ⏳ Da fare | +| F4: Scomposizione MpDataService | 5 nuovi + 1 esistente | ~3516 | Alto | ⏳ Da fare | +| F5: Build & Verifica | — | — | Basso | ✅ 0 errori | ## Stato Completato ### ✅ Fase 1: Anti-Pattern Controller — COMPLETATA -Rimosso `static IConfiguration _configuration` da tutti i 4 controller e rimossa la dead assignment dai costruttori: +Rimosso `static IConfiguration _configuration` da tutti i 4 controller. -| Controller | Prima | Dopo | -|------------|-------|------| -| `IOBController` | `IConfiguration + static field` | Nessun param (dead code), field rimosso | -| `BenchController` | `IConfiguration + static field` | Solo `MpDataService`, field rimosso | -| `RecipeController` | `IConfiguration + static field` | Solo `MpDataService`, field rimosso | -| `RecipeArchiveController` | `IConfiguration + static field` | Solo `MpDataService`, field rimosso | +### ✅ Fase 2: FusionCache + Redis Backplane — COMPLETATA -**Build risultato:** ✅ 0 errori, 12 warnings (solo nullable reference, nessun warning statico residuo nei controller) +- Pacchetti FusionCache aggiunti a `MP.IOC.csproj` +- `IFusionCache` configurato con L1 Memory + L2 Redis + Backplane in `Program.cs` +- Campo `_cache` aggiunto a `MpDataService` con helper `GetOrFetchAsync` +- **18 metodi migrati** dal pattern manuale Redis → FusionCache +- Build: ✅ **0 errori**, 12 warnings (tutti nullable reference preesistenti) -### 📋 Fase 2+: In Attesa +## Invntario Completo Metodi (Post-Refactoring) -Prossimo step: abilitare FusionCache con Redis backplane (Fase 2) — richiede approvazione architetturale. +### ✅ MIGRATI A FUSIONCACHE (18 metodi) + +| # | Metodo | Fetch From | Tag | +|---|--------|------------|-----| +| 1 | `AnagStatiGetAllAsync` | `IocDbController.AnagStatiGetAllAsync()` | `Utils.redisAnagStati` | +| 2 | `ArticoliGetLastByMaccAsync` | `IocDbController.ArticoliGetLastByMaccAsync()` | `Utils.redisArtList:Last:{idxMacc}` | +| 3 | `ConfigGetAllAsync` | `IocDbController.ConfigGetAllAsync()` | `Utils.redisConfKey` | +| 4 | `DecNumArtGetFiltAsync` | `IocDbController.DecNumArtGetFiltAsync()` | `Utils.redisDecNumArtKey:{tag}` | +| 5 | `DossierLastByMachAsync` | `IocDbController.DossGetLastByMaccAsync()` | `Utils.redisDossByMacLast:{idxMacc}` | +| 6 | `GetLastOdlAsync` | `IocDbController.OdlLastByMaccAsync()` | `Utils.redisOdlLastByMac:{idxMacc}` | +| 7 | `ListValuesFilt` | `IocDbController.ListValuesFiltAsync()` | `Utils.redisConfFlux:{tag}` | +| 8 | `Macchine2SlaveGetAllAsync` | `IocDbController.Macchine2SlaveAsync()` | `Utils.redisBaseAddr:M2STab` | +| 9 | `MacchineGetFilt` | `_productionRepository.MacchineGetFiltAsync()` | `Utils.redisMacList:{keyGrp}` | +| 10 | `MacchineRecipeArchive` | `_productionRepository.MacchineGetFiltAsync("*")` → LINQ | `Utils.redisMacRecipePath:{idxMacc}` | +| 11 | `MseGetAllAsync` | `IocDbController.MseGetAllAsync(maxAge)` | `Constants.redisMseKey` (+ forceDb) | +| 12 | `OdlCurrByMaccAsync` | `IocDbController.OdlCurrByMaccAsync()` | `Utils.redisOdlList:Current:{idxMacc}` | +| 13 | `POdlGetByKey` | `_productionRepository.PODL_getByKeyAsync()` | `Utils.redisPOdlByPOdl:{idxPODL}` | +| 14 | `POdlGetByMaccArtAsync` | `IocDbController.POdlGetByMaccArtAsync()` | `Utils.redisPOdlByMaccArt:{idxMacc}...` | +| 15 | `ConfFluxMach` | `IocDbController.ConfFluxFiltAsync()` → LINQ | `Utils.redisConfFlux:{tag}` | +| 16 | `FluxLogFirstByMachAsync` | `IocDbController.FluxLogFirstByMaccAsync()` → LINQ | `Utils.redisFluxLogFirstByMac:{key}` | +| 17 | `FluxLogGetLastByMachAsync` | `IocDbController.FluxLogGetLastFiltAsync()` → LINQ | `Utils.redisFluxLogByMac:{key}` | + +### ❌ NON MIGRATI (Scrittura / TTL / Hash-only — 30+ metodi) + +Questi metodi usano Redis ma **NON** hanno pattern cache-aside (StringGet → DB → StringSet). Sono dati transituali IOB o write-through: + +| Categoria | Metodi | Motivazione | +|-----------|--------|-------------| +| **Scrittura Only (StringSet)** | `MachineParamListSetAsync`, `SaveMachine2Iob`, `SaveMachineIobConf`, `SetIobConfYamlAsync`, `SetIobMemMap`, `UpsertCurrObjItemsAsync` | Solo StringSet, zero StringGet | +| **Lock/TTL-based** | `AutoStartOdlAsync` (veto), `ScriviKeepAliveAsync` (TTL 30s) | Pattern di lock, non di cache | +| **Hash-based IOB State** | `MachineParamListAsync`, `AddCheckTask4Machine`, `AddOptPar4Machine`, `AddTask4Machine`, `AddTask4MacListAsync`, `mOptParMacchina`, `mSavedTaskMacchina`, `mTaskMacchina`, `mTaskMacchinaAsync`, `mDatiMacchineAsync`, `mTabMSMIAsync`, `StateMachInByKeyAsync`, `ValoreSmiAsync`, `StateMachInByKeyAsync` | Redis Hash per dati in tempo reale IOB (transazionali), non Database-backed | +| **Hash Reset Helpers** | `ResetDatiMacchinaAsync`, `resetMSMIAsync`, `resetSMIAsync` | Reset di tabelle Hash Redis, non cache | +| **Redis GetHash Helpers** | `RedisGetHash`, `RedisGetHashAsync`, `RedisGetHashDict`, `RedisGetHashDictAsync`, `RedisSetHash`, `RedisSetHashAsync`, `RedisSetHashDict`, `RedisSetHashDictAsync`, `RedisCountKey`, `RedisDelKey`, `RedisKeyPresent`, `RedisGetHashField` | Helper utility Redis, non metodi di business | + +### ⚠️ DA VERIFICARE (Pattern atipico - 4 metodi) + +Questi metodi hanno Redis ma il pattern non è lo standard cache-aside. Vanno analizzati caso per caso: + +| # | Metodo | Riga | Motivo Verifica | +|---|--------|------|-----------------| +| 1 | `GetCurrOdlAsync` | ~892 | Chiama `GetCurrOdlByProdAsync()` (metodo interno complesso), non `IocDbController.XXX()` direttamente. Pattern: StringGet → DB → StringSet. Verificare se `GetCurrOdlByProdAsync` è una stored procedure. | +| 2 | `StatoProdMacchinaAsync` (private) | ~3267 | Ha parametro `forceDb`, cache breve (60s pattern). Pattern valido ma private. | +| 3 | `verificaIdxMacchinaAsync` | ~3267 | Verifica esistenza macchina, non un cache-aside standard. | +| 4 | `ListMasterAsync` / `ListSlaveAsync` | ~2820/~2840 | Cache-aside valido ma derivano da `Macchine2SlaveGetAllAsync()` (già migrata). Potrebbero essere rimossi in favore di un unico repository. | ## Priorità Operativa -1. **F1 immediata** — zero rischio, fix 4 controller statici -2. **F2 parallela** — abilitare FusionCache con Redis backplane (config) + migrare i metodi più usati -3. **F3 dopo F2** — decomporre MpIocController una volta che il caching è standardizzato -4. **F4 opzionale** — scomporare MpDataService quando il resto è stabile +1. ~~**F1 immediata**~~ — ✅ Completata +2. ~~**F2 FusionCache**~~ — ✅ Completata (18 metodi migrati) +3. **F3 Repository IoC** — Prossimo step maggiore: decomporre `MpIocController` (1480 righe, 82 metodi) in 7-10 repository specialistici +4. **F4 Scomposizione MpDataService** — Quando Fase 3 è stabile, dividere il monolite da 3516 righe +5. **Verifica metodi atipici** — I 4 metodi con pattern non-standards vanno valutati manualmente ## Note -- MpIocController usa già `IDbContextFactory` correttamente — il problema è che è un Singleton e lo si chiama da un altro Singleton (MpDataService) -- IIocRepository (già esistente) ha solo 16 metodi ed è mai usato da MpDataService -- I repository SPEC (Anag, Dossier, FluxLog, Production) sono già usati da alcuni metodi — sfruttare questi dove possibile -- La struttura MP.Data/Controllers/ è ingannevole: MpIocController è un servizio, non un controller Web API +- MpIocController usa già `IDbContextFactory` correttamente — il problema è che è un Singleton usato da un altro Singleton (MpDataService) +- `IIocRepository` (già esistente) ha solo 16 metodi ed è mai usato da MpDataService +- I repository SPEC (Anag, Dossier, FluxLog, Production) sono già usati da alcuni metodi — sfruttarli dove possibile +- La struttura `MP.Data/Controllers/` è ingannevole: `MpIocController` è un servizio, non un controller Web API +- `redisConnAdmin` (connessione Redis admin) sembra non essere mai usata — verificare eliminazione +- **Build finale:** ✅ 0 errori, 12 warnings (tutti nullable reference preesistenti)