Update IOC con nuovi metodi cache FusionCache (int est...)

This commit is contained in:
Samuele Locatelli
2026-06-04 18:23:13 +02:00
parent a6900f01a1
commit 10e39b1c32
8 changed files with 397 additions and 522 deletions
+177 -366
View File
@@ -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<MpDataService> 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
/// <summary>
/// Restituisce l'anagrafica STATI per intero
/// </summary>
/// <returns></returns>
public async Task<List<AnagStatiModel>> AnagStatiGetAllAsync()
{
List<AnagStatiModel> dbResult = new List<AnagStatiModel>();
// cerco in redis...
var currKey = Utils.redisAnagStati;
RedisValue rawData = await redisDb.StringGetAsync(currKey);
if (rawData.HasValue)
{
dbResult = JsonConvert.DeserializeObject<List<AnagStatiModel>>($"{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<AnagStatiModel>(),
tagList: [Utils.redisAnagStati]
);
}
/// <summary>
@@ -351,33 +344,13 @@ namespace MP.IOC.Data
/// <returns></returns>
public async Task<List<AnagArticoliModel>> ArticoliGetLastByMaccAsync(string idxMacc)
{
List<AnagArticoliModel>? result = new List<AnagArticoliModel>();
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<List<AnagArticoliModel>>($"{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<AnagArticoliModel>();
}
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<AnagArticoliModel>(),
tagList: [Utils.redisArtList]
);
}
/// <summary>
@@ -792,54 +765,24 @@ namespace MP.IOC.Data
public async Task<List<ConfigModel>> ConfigGetAllAsync()
{
Stopwatch stopWatch = new Stopwatch();
stopWatch.Start();
List<ConfigModel>? result = new List<ConfigModel>();
// cerco in redis...
RedisValue rawData = await redisDb.StringGetAsync(Utils.redisConfKey);
if (!string.IsNullOrEmpty($"{rawData}"))
{
result = JsonConvert.DeserializeObject<List<ConfigModel>>($"{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<ConfigModel>();
}
return result;
return await GetOrFetchAsync(
operationName: "ConfigGetAllAsync",
cacheKey: Utils.redisConfKey,
expiration: getRandTOut(redisLongTimeCache),
fetchFunc: async () => (await IocDbController.ConfigGetAllAsync()) ?? new List<ConfigModel>(),
tagList: [Utils.redisConfKey]
);
}
public async Task<List<DecNumArticoliModel>> DecNumArtGetFiltAsync(string codArt = "")
{
List<DecNumArticoliModel> 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<List<DecNumArticoliModel>>($"{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<DecNumArticoliModel>(),
tagList: [Utils.redisDecNumArtKey]
);
}
/// <summary>
@@ -850,26 +793,13 @@ namespace MP.IOC.Data
/// <returns></returns>
public async Task<List<DossierModel>> DossierLastByMachAsync(string idxMacchina)
{
List<DossierModel> result = new List<DossierModel>();
var currKey = $"{Utils.redisDossByMacLast}:{idxMacchina}";
RedisValue rawData = await redisDb.StringGetAsync(currKey);
if (rawData.HasValue)
{
result = JsonConvert.DeserializeObject<List<DossierModel>>($"{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<DossierModel>();
}
return result;
return await GetOrFetchAsync(
operationName: "DossierLastByMachAsync",
cacheKey: $"{Utils.redisDossByMacLast}:{idxMacchina}",
expiration: getRandTOut(redisLongTimeCache),
fetchFunc: async () => (await IocDbController.DossGetLastByMaccAsync(idxMacchina)) ?? new List<DossierModel>(),
tagList: [Utils.redisDossByMacLast]
);
}
/// <summary>
@@ -986,22 +916,13 @@ namespace MP.IOC.Data
/// <returns></returns>
public async Task<ODLExpModel> 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<ODLExpModel>($"{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]
);
}
/// <summary>
@@ -1165,36 +1086,25 @@ namespace MP.IOC.Data
/// <returns></returns>
public async Task<List<Macchine2SlaveModel>> Macchine2SlaveGetAllAsync()
{
List<Macchine2SlaveModel>? result = new List<Macchine2SlaveModel>();
string currKey = $"{Utils.redisBaseAddr}:M2STab";
// cerco in redis dato valOut sel macchina...
RedisValue rawData = await redisDb.StringGetAsync(currKey);
if (rawData.HasValue)
{
result = JsonConvert.DeserializeObject<List<Macchine2SlaveModel>>($"{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<MpIocController>();
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<Macchine2SlaveModel>();
}
return result;
if (useFactory)
{
await using var scope = _scopeFactory.CreateAsyncScope();
var mtcService = scope.ServiceProvider.GetRequiredService<MpIocController>();
return await mtcService.Macchine2SlaveAsync() ?? new List<Macchine2SlaveModel>();
}
else
{
return await IocDbController.Macchine2SlaveAsync() ?? new List<Macchine2SlaveModel>();
}
},
tagList: [Utils.redisBaseAddr]
);
}
/// <summary>
@@ -1204,34 +1114,14 @@ namespace MP.IOC.Data
/// <returns></returns>
public async Task<List<MacchineModel>> MacchineGetFilt(string codGruppo)
{
List<MacchineModel>? result = new List<MacchineModel>();
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<List<MacchineModel>>($"{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<MacchineModel>();
}
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<MacchineModel>(),
tagList: [Utils.redisMacList]
);
}
/// <summary>
@@ -1241,32 +1131,18 @@ namespace MP.IOC.Data
/// <returns></returns>
public async Task<string> 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<string>($"{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]
);
}
/// <summary>
@@ -1450,31 +1326,17 @@ namespace MP.IOC.Data
/// <returns></returns>
public async Task<List<MappaStatoExplModel>> MseGetAllAsync(bool forceDb = false)
{
Stopwatch sw = new Stopwatch();
string source = "DB";
sw.Start();
List<MappaStatoExplModel>? result = new List<MappaStatoExplModel>();
// cerco in _redisConn...
RedisValue rawData = await redisDb.StringGetAsync(Constants.redisMseKey);
if (rawData.HasValue && !forceDb)
if (forceDb)
{
result = JsonConvert.DeserializeObject<List<MappaStatoExplModel>>($"{rawData}") ?? new();
source = "REDIS";
return (await IocDbController.MseGetAllAsync(maxAge)) ?? new List<MappaStatoExplModel>();
}
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<MappaStatoExplModel>();
}
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<MappaStatoExplModel>(),
tagList: [Constants.redisMseKey]
);
}
/// <summary>
@@ -1576,33 +1438,13 @@ namespace MP.IOC.Data
public async Task<ODLExpModel> 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<ODLExpModel>($"{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]
);
}
/// <summary>
@@ -1612,77 +1454,31 @@ namespace MP.IOC.Data
/// <returns></returns>
public async Task<PODLModel> 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<PODLModel>($"{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<List<PODLExpModel>> POdlGetByMaccArtAsync(string idxMacchina, string codArticolo, string codGruppo, bool onlyFree)
{
List<PODLExpModel> result = new List<PODLExpModel>();
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<List<PODLExpModel>>($"{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<PODLExpModel>();
}
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<PODLExpModel>(),
tagList: [Utils.redisPOdlByMaccArt]
);
}
/// <summary>
@@ -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;
/// <summary>
/// Cache Fusion (Memory + Redis + DB)
/// </summary>
private readonly IFusionCache _cache;
#endregion Private Fields
#region Private Methods
@@ -2776,6 +2576,49 @@ namespace MP.IOC.Data
/// <param name="Value"></param>
/// <param name="MatrOpr"></param>
/// <param name="pallet"></param>
/// <summary>
/// Helper standard FusionCache - recupero da L1/L2/L3 con tracking activity
/// </summary>
private async Task<T> GetOrFetchAsync<T>(string operationName, string cacheKey, Func<Task<T>> fetchFunc, TimeSpan expiration, params string[] tagList)
{
using var activity = new ActivitySource("MP.IOC.Tracer").StartActivity(operationName);
string source;
var tryGet = await _cache.TryGetAsync<T>(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<T>(
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
/// <returns></returns>
private async Task<List<string>> ConfFluxMach(string idxMacchina)
{
List<string> resultList = new List<string>();
string tag = string.IsNullOrEmpty(idxMacchina) ? "ALL" : idxMacchina;
var currKey = $"{Utils.redisConfFlux}:{tag}";
RedisValue rawData = await redisDb.StringGetAsync(currKey);
if (rawData.HasValue)
{
resultList = JsonConvert.DeserializeObject<List<string>>($"{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<string>(),
tagList: [Utils.redisConfFlux]
);
}
/// <summary>
@@ -2895,29 +2722,13 @@ namespace MP.IOC.Data
/// <returns></returns>
private async Task<List<DateTime>> FluxLogFirstByMachAsync(string idxMacchina, int numMax = 10)
{
List<DateTime> resultList = new List<DateTime>();
var currKey = $"{Utils.redisFluxByMacFirst}:{idxMacchina}";
RedisValue rawData = await redisDb.StringGetAsync(currKey);
if (rawData.HasValue)
{
resultList = JsonConvert.DeserializeObject<List<DateTime>>($"{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<DateTime>();
}
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<DateTime>(),
tagList: [Utils.redisFluxByMacFirst]
);
}
/// <summary>
+6 -1
View File
@@ -4,7 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<Version>8.16.2606.414</Version>
<Version>8.16.2606.417</Version>
</PropertyGroup>
<ItemGroup>
@@ -31,10 +31,15 @@
<PackageReference Include="EgwCoreLib.Razor" />
<PackageReference Include="EgwCoreLib.Utils" />
<PackageReference Include="Microsoft.AspNetCore.Components" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="NLog" />
<PackageReference Include="NLog.Web.AspNetCore" />
<PackageReference Include="Snappier" />
<PackageReference Include="Swashbuckle.AspNetCore" />
<PackageReference Include="ZiggyCreatures.FusionCache" />
<PackageReference Include="ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis" />
<PackageReference Include="ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson" />
</ItemGroup>
<ItemGroup>
+15 -1
View File
@@ -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<IConnectionMultiplexer>(redisMux);
// Distributed cache (necessario per FusionCache L2 Redis)
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = confRedis;
});
// oggetto principale accesso dati
//builder.Services.AddScoped<MpDataService>();
builder.Services.AddSingleton<MpDataService>();
@@ -99,8 +107,14 @@ builder.Services.AddSingleton<MpDataService>();
// 1. Registra il serializzatore NewtonsoftJson per FusionCache
builder.Services.AddSingleton<IFusionCacheSerializer>(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<IDistributedCache>())
.WithSerializer(new FusionCacheNewtonsoftJsonSerializer())
.WithBackplane(new RedisBackplane(new RedisBackplaneOptions
{
ConnectionMultiplexerFactory = () => Task.FromResult<IConnectionMultiplexer>(redisMux)
}))
.WithDefaultEntryOptions(options =>
{
// Durata di default dei dati in memoria
+67 -11
View File
@@ -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<T>()` — 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`)
+1 -1
View File
@@ -1,6 +1,6 @@
<body>
<i>Modulo MP-IOC </i>
<h4>Versione: 8.16.2606.414</h4>
<h4>Versione: 8.16.2606.417</h4>
<br /> Note di rilascio:
<ul>
<li>
+1 -1
View File
@@ -1 +1 @@
8.16.2606.414
8.16.2606.417
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>8.16.2606.414</version>
<version>8.16.2606.417</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>
+129 -140
View File
@@ -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<IMpIocService, MpIocController>(); // 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<IConnectionMultiplexer>(redisMux);
// L1 Memory + L2 Redis Distributed + Redis Backplane
builder.Services.AddStackExchangeRedisCache(options =>
options.Configuration = confRedis);
builder.Services.AddFusionCache()
.WithDistributedCache(sp => sp.GetRequiredService<IDistributedCache>())
.WithSerializer(new FusionCacheNewtonsoftJsonSerializer())
.WithBackplane(new RedisBackplane(new RedisBackplaneOptions {
ConnectionMultiplexerFactory = () => Task.FromResult(redisMux)
}));
ConnectionMultiplexerFactory = () => Task.FromResult<IConnectionMultiplexer>(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<T>`
- **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)