diff --git a/Directory.Packages.props b/Directory.Packages.props
index 0dd9728d..4613ee5e 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -33,6 +33,7 @@
+
@@ -53,5 +54,8 @@
+
+
+
\ No newline at end of file
diff --git a/EgwCoreLib.Lux.Data/EgwCoreLib.Lux.Data.csproj b/EgwCoreLib.Lux.Data/EgwCoreLib.Lux.Data.csproj
index fed9128f..a968fd4f 100644
--- a/EgwCoreLib.Lux.Data/EgwCoreLib.Lux.Data.csproj
+++ b/EgwCoreLib.Lux.Data/EgwCoreLib.Lux.Data.csproj
@@ -49,6 +49,9 @@
+
+
+
diff --git a/EgwCoreLib.Lux.Data/Services/Admin/IVocabolarioService.cs b/EgwCoreLib.Lux.Data/Services/Admin/IVocabolarioService.cs
index d56d56ce..4de5784b 100644
--- a/EgwCoreLib.Lux.Data/Services/Admin/IVocabolarioService.cs
+++ b/EgwCoreLib.Lux.Data/Services/Admin/IVocabolarioService.cs
@@ -47,10 +47,10 @@
///
/// Traduzione lemma nella lingua desiderata
///
- ///
///
+ ///
///
- string Traduci(string lingua, string lemma);
+ string Traduci(string lemma, string lingua);
///
/// Upsert record Vocabolario
diff --git a/EgwCoreLib.Lux.Data/Services/Admin/VocabolarioService.cs b/EgwCoreLib.Lux.Data/Services/Admin/VocabolarioService.cs
index f4825cb1..79e0fe92 100644
--- a/EgwCoreLib.Lux.Data/Services/Admin/VocabolarioService.cs
+++ b/EgwCoreLib.Lux.Data/Services/Admin/VocabolarioService.cs
@@ -1,5 +1,12 @@
-namespace EgwCoreLib.Lux.Data.Services.Admin
+using ZiggyCreatures.Caching.Fusion;
+
+namespace EgwCoreLib.Lux.Data.Services.Admin
{
+ ///
+ /// Service per la gestione del vocabolario traduzioni interfaccia.
+ /// Il dizionario viene caricato in-memory all'avvio dal BackgroundService.
+ /// Ogni modifica CRUD aggiorna il dizionario senza passare per Redis.
+ ///
public class VocabolarioService : BaseServ, IVocabolarioService
{
#region Public Constructors
@@ -7,10 +14,12 @@
public VocabolarioService(
IConfiguration config,
IConnectionMultiplexer redis,
- IVocabolarioRepository repo) : base(config, redis)
+ IFusionCache cache,
+ IDbContextFactory factory) : base(config, redis)
{
_className = "Vocabolario";
- _repo = repo;
+ _cache = cache;
+ _factory = factory;
}
#endregion Public Constructors
@@ -22,14 +31,42 @@
{
return await TraceAsync($"{_className}.Clone", async (activity) =>
{
- var result = await _repo.CloneAsync(linguaOrig, linguaDest);
+ var ctx = await _factory.CreateDbContextAsync();
+ var list = await ctx.DbSetVocabolario
+ .Where(x => x.Lingua == linguaOrig)
+ .ToListAsync();
- if (result)
+ if (!list.Any()) return false;
+
+ await using var tx = await ctx.Database.BeginTransactionAsync();
+ bool done = false;
+ try
{
- await ClearCacheAsync($"{_redisBaseKey}:{_className}:*");
- await _syncDictFromRepo();
+ var recLang = await ctx.DbSetLingua.FirstOrDefaultAsync(x => x.Lingua == linguaDest);
+ if (recLang == null)
+ {
+ recLang = new LinguaModel { Lingua = linguaDest, Note = $"Cloned from {linguaOrig}" };
+ await ctx.DbSetLingua.AddAsync(recLang);
+ await ctx.SaveChangesAsync();
+ }
+
+ var listNew = list.Select(x => new VocabolarioModel
+ {
+ Lingua = linguaDest,
+ Lemma = x.Lemma,
+ Traduzione = x.Traduzione
+ }).ToList();
+
+ ctx.DbSetVocabolario.AddRange(listNew);
+ done = await ctx.SaveChangesAsync() > 0;
+ await tx.CommitAsync();
}
- return result;
+ catch
+ {
+ await tx.RollbackAsync();
+ throw;
+ }
+ return done;
});
}
@@ -38,15 +75,14 @@
{
return await TraceAsync($"{_className}.Delete", async (activity) =>
{
- var dbResult = await _repo.GetByIdAsync(rec2del.Lingua, rec2del.Lemma);
- if (dbResult == null) return false;
+ var ctx = await _factory.CreateDbContextAsync();
+ var dbRec = await ctx.DbSetVocabolario
+ .FirstOrDefaultAsync(x => x.Lingua == rec2del.Lingua && x.Lemma == rec2del.Lemma);
- bool success = await _repo.DeleteAsync(dbResult);
- if (success)
- {
- await ClearCacheAsync($"{_redisBaseKey}:{_className}:*");
- await _syncDictFromRepo();
- }
+ if (dbRec == null) return false;
+
+ ctx.DbSetVocabolario.Remove(dbRec);
+ bool success = await ctx.SaveChangesAsync() > 0;
return success;
});
}
@@ -58,7 +94,11 @@
{
return await GetOrSetCacheAsync(
$"{_redisBaseKey}:{_className}:ALL",
- async () => await _repo.GetAllAsync()
+ async () =>
+ {
+ var ctx = await _factory.CreateDbContextAsync();
+ return await ctx.DbSetVocabolario.ToListAsync();
+ }
);
});
}
@@ -70,7 +110,12 @@
{
return await GetOrSetCacheAsync(
$"{_redisBaseKey}:{_className}:GetById:{lingua}:{lemma}",
- async () => await _repo.GetByIdAsync(lingua, lemma)
+ async () =>
+ {
+ var ctx = await _factory.CreateDbContextAsync();
+ return await ctx.DbSetVocabolario
+ .FirstOrDefaultAsync(x => x.Lingua == lingua && x.Lemma == lemma);
+ }
);
});
}
@@ -78,24 +123,14 @@
///
public async Task> GetByLang(string lingua)
{
- // Se il dizionario in-memory è caricato, restituisce subito da RAM
- EnsureDictLoaded();
- if (_translations.TryGetValue(lingua, out var langDict) && langDict.Count > 0)
- {
- return langDict
- .Select(kvp => new VocabolarioModel
- {
- Lingua = lingua,
- Lemma = kvp.Key,
- Traduzione = kvp.Value
- })
- .ToList();
- }
-
- // Fallback: recupero dal DB con cache
+ // Fallback: DB + Redis cache
return await GetOrSetCacheAsync(
$"{_redisBaseKey}:{_className}:GetByLang:{lingua}",
- async () => await _repo.GetByLang(lingua)
+ async () =>
+ {
+ var ctx = await _factory.CreateDbContextAsync();
+ return await ctx.DbSetVocabolario.Where(o => o.Lingua == lingua).ToListAsync();
+ }
);
}
@@ -106,52 +141,105 @@
{
return await GetOrSetCacheAsync(
$"{_redisBaseKey}:{_className}:Lingue",
- async () => await _repo.ListLingueAsync()
+ async () =>
+ {
+ var ctx = await _factory.CreateDbContextAsync();
+ return await ctx.DbSetLingua.ToListAsync();
+ }
);
});
}
///
- public string Traduci(string lingua, string lemma)
+ ///
+ /// Esegue traduzione dato vocabolario da Lingua + Lemma
+ ///
+ ///
+ ///
+ ///
+ public string Traduci(string lemma, string lingua = "IT")
{
- EnsureDictLoaded();
+ if (string.IsNullOrWhiteSpace(lemma)) return string.Empty;
+ if (string.IsNullOrWhiteSpace(lingua)) return lemma;
- if (_translations.TryGetValue(lingua, out var langDict)
- && langDict.TryGetValue(lemma, out var traduzione))
+ string linguaKey = lingua.ToLowerInvariant().Trim();
+ string cacheKey = $"vocab:{linguaKey}";
+
+ var ctx = _factory.CreateDbContext();
+
+ // FusionCache gestisce il lock e recupera l'intero dizionario della lingua.
+ // Se è in L1 (Memory), restituisce l'oggetto C# istantaneamente.
+ // Se non c'è, passa a L2 (Redis) o invoca la factory per caricarlo.
+ var dizionarioLingua = _cache.GetOrSet>(
+ cacheKey,
+ _ => ctx
+ .DbSetVocabolario
+ .Where(x => x.Lingua == linguaKey)
+ .ToDictionary(d => d.Lemma, d => d.Traduzione),
+ options => options
+ //.SetDuration(TimeSpan.FromHours(8)) // Durata logica della cache
+ //.SetFailSafe(true, TimeSpan.FromHours(1)) // Se Redis/DB è giù, usa i vecchi dati L1
+ .SetDuration(TimeSpan.FromMinutes(5)) // Durata logica della cache
+ .SetFailSafe(true, TimeSpan.FromMinutes(15)) // Se Redis/DB è giù, usa i vecchi dati L1
+ );
+
+ // Ricerca O(1) nel dizionario in memoria
+ if (dizionarioLingua != null && dizionarioLingua.TryGetValue(lemma, out var traduzione))
{
return traduzione;
}
- return lemma;
+ // Fallback: se la parola non è censita inserisce su DB
+ string answ = $"[[{lingua}_{lemma}]]";
+ var newRec = new VocabolarioModel()
+ {
+ Lingua = lingua,
+ Lemma = lemma,
+ Traduzione = answ
+ };
+ try
+ {
+ ctx.DbSetVocabolario.Add(newRec);
+ ctx.SaveChanges();
+ }
+ catch (Exception exc)
+ {
+ Log.Error(exc);
+ }
+
+ // ...e restituisce il lemma originale (lingua + lemma)
+ return answ;
}
+ ///
+ /// Oggetto gestione FusionCache
+ ///
+ protected readonly IFusionCache _cache;
+
///
public async Task UpsertAsync(VocabolarioModel upsRec)
{
return await TraceAsync($"{_className}.Upsert", async (activity) =>
{
- var currRec = await _repo.GetByIdAsync(upsRec.Lingua, upsRec.Lemma);
+ var ctx = await _factory.CreateDbContextAsync();
+ var currRec = await ctx.DbSetVocabolario
+ .FirstOrDefaultAsync(x => x.Lingua == upsRec.Lingua && x.Lemma == upsRec.Lemma);
string operation = "UPDATE";
bool success = false;
if (currRec != null)
{
- success = await _repo.UpdateAsync(upsRec);
+ ctx.Entry(currRec).CurrentValues.SetValues(upsRec);
+ success = await ctx.SaveChangesAsync() > 0;
}
else
{
operation = "INSERT";
- success = await _repo.AddAsync(upsRec);
+ await ctx.DbSetVocabolario.AddAsync(upsRec);
+ success = await ctx.SaveChangesAsync() > 0;
}
activity?.SetTag("db.operation", operation);
-
- if (success)
- {
- await ClearCacheAsync($"{_redisBaseKey}:{_className}:*");
- await _syncDictFromRepo();
- }
-
return success;
});
}
@@ -161,18 +249,42 @@
{
return await TraceAsync($"{_className}.UpsertMany", async (activity) =>
{
- string operation = "MERGE";
- bool success = false;
- success = await _repo.UpsertManyAsync(upsList);
+ var ctx = await _factory.CreateDbContextAsync();
- activity?.SetTag("db.operation", operation);
+ var listLang = upsList.Select(d => d.Lingua).Distinct().ToList();
+ var listLemmi = upsList.Select(d => d.Lemma).Distinct().ToList();
+ var dbList = await ctx.DbSetVocabolario
+ .Where(t => listLang.Contains(t.Lingua) && listLemmi.Contains(t.Lemma))
+ .ToListAsync();
- if (success)
+ var listExist = dbList
+ .Where(t => upsList.Any(d => d.Lingua == t.Lingua && d.Lemma == t.Lemma))
+ .ToList();
+
+ foreach (var dto in upsList)
{
- await ClearCacheAsync($"{_redisBaseKey}:{_className}:*");
- await _syncDictFromRepo();
+ var existing = listExist
+ .FirstOrDefault(r => r.Lingua == dto.Lingua && r.Lemma == dto.Lemma);
+
+ if (existing != null)
+ {
+ existing.Traduzione = dto.Traduzione;
+ }
+ else
+ {
+ await ctx.DbSetVocabolario.AddAsync(new VocabolarioModel
+ {
+ Lingua = dto.Lingua,
+ Lemma = dto.Lemma,
+ Traduzione = dto.Traduzione
+ });
+ }
}
+ string operation = "MERGE";
+ bool success = await ctx.SaveChangesAsync() > 0;
+
+ activity?.SetTag("db.operation", operation);
return success;
});
}
@@ -182,88 +294,12 @@
#region Private Fields
private readonly string _className;
- private readonly IVocabolarioRepository _repo;
-
- private readonly ConcurrentDictionary> _translations
- = new(StringComparer.OrdinalIgnoreCase);
-
- private readonly SemaphoreSlim _initLock = new(1, 1);
- private bool _initialized = false;
+ private readonly IDbContextFactory _factory;
#endregion Private Fields
#region Private Methods
- ///
- /// Carica il dizionario in-memory dal repository.
- /// Struttura: Lingua -> Dictionary<Lemma, Traduzione>.
- /// Chiamato in lazy-load alla prima lettura e dopo ogni modifica CRUD.
- ///
- private async Task LoadDictFromRepoAsync()
- {
- var rawData = await _repo.GetAllAsync();
-
- var newTrans = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase);
-
- foreach (var row in rawData)
- {
- var lingua = row.Lingua;
- var lemma = row.Lemma;
- var traduzione = row.Traduzione;
-
- var langDict = newTrans.GetOrAdd(lingua, _ => new Dictionary(StringComparer.OrdinalIgnoreCase));
- langDict[lemma] = traduzione;
- }
-
- _translations.Clear();
- foreach (var kvp in newTrans)
- {
- _translations[kvp.Key] = kvp.Value;
- }
- }
-
- ///
- /// Caricamento lazy con doppio check lock.
- ///
- private void EnsureDictLoaded()
- {
- if (_initialized) return;
-
- _initLock.Wait();
- try
- {
- if (_initialized) return;
-
- //LoadDictFromRepoAsync().GetAwaiter().GetResult();
- _initialized = true;
- }
- finally
- {
- _initLock.Release();
- }
- }
-
- ///
- /// Aggiorna il dizionario in-memory dopo operazione CRUD.
- /// Ricarica l'intero vocabolario dal repo (senza passare per Redis).
- ///
- private async Task _syncDictFromRepo()
- {
- try
- {
- // Ricarico tutto dal repo (senza passare per la cache Redis)
- await LoadDictFromRepoAsync();
-
- // Se il dizionario era già stato caricato, mantengo _initialized a true
- // (serve a non rifarlo alla prossima chiamata lazy)
- }
- catch
- {
- // Silenzio: se il load fallisce (DB down), resto con lo stato precedente.
- // GetByLang/Traduci avranno come fallback il vecchio dizionario o la cache Redis.
- }
- }
-
#endregion Private Methods
}
}
diff --git a/Lux.API/Lux.API.csproj b/Lux.API/Lux.API.csproj
index 76694d86..d9d82881 100644
--- a/Lux.API/Lux.API.csproj
+++ b/Lux.API/Lux.API.csproj
@@ -4,7 +4,7 @@
net8.0
enable
enable
- 1.1.2606.0311
+ 1.1.2606.0409
diff --git a/Lux.API/Program.cs b/Lux.API/Program.cs
index 0c5178c9..4ad68b56 100644
--- a/Lux.API/Program.cs
+++ b/Lux.API/Program.cs
@@ -1,9 +1,13 @@
using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Caching.Distributed;
using Microsoft.OpenApi.Models;
using NLog.Targets;
using NLog.Web;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
+using ZiggyCreatures.Caching.Fusion;
+using ZiggyCreatures.Caching.Fusion.Backplane.StackExchangeRedis;
+using ZiggyCreatures.Caching.Fusion.Serialization.NewtonsoftJson;
//using System.Text.Json.Serialization;
var builder = WebApplication.CreateBuilder(args);
@@ -21,7 +25,16 @@ logger.Info($"Current ASPNETCORE_ENVIRONMENT: {env.EnvironmentName}");
// costruzione connectionMultiplexer redis...
string connStr = configuration.GetConnectionString("Redis") ?? "localhost";
-ConnectionMultiplexer redisConn = ConnectionMultiplexer.Connect(connStr);
+IConnectionMultiplexer redisMPlex = ConnectionMultiplexer.Connect(connStr);
+
+// ✅ FusionCache
+builder.Services.AddFusionCache()
+ .WithDistributedCache(sp => sp.GetRequiredService())
+ .WithSerializer(new FusionCacheNewtonsoftJsonSerializer())
+ .WithBackplane(new RedisBackplane(new RedisBackplaneOptions
+ {
+ ConnectionMultiplexerFactory = () => Task.FromResult(redisMPlex)
+ }));
// ====================================================================
// Setup Tracing e Telemetria...
@@ -51,7 +64,7 @@ if (otelEnabled)
.AddSource("Lux.UI")
.AddAspNetCoreInstrumentation(options => { options.Filter = ctx => !ctx.Request.Path.StartsWithSegments("/health"); })
.AddEntityFrameworkCoreInstrumentation()
- .AddRedisInstrumentation(redisConn);
+ .AddRedisInstrumentation(redisMPlex);
// ====================================================================
// ESPORTAZIONE DI RETE (Solo Livelli 1 e 2)
@@ -129,7 +142,7 @@ builder.Services.AddSwaggerGen(c =>
// registro connMultiplexer REDIS
-builder.Services.AddSingleton(redisConn);
+builder.Services.AddSingleton(redisMPlex);
var connectionString = builder.Configuration.GetConnectionString("Lux.All") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
// DataLayerContext (manca!)
diff --git a/Lux.UI/Components/BaseComp.cs b/Lux.UI/Components/BaseComp.cs
index d3aaada2..a093a20c 100644
--- a/Lux.UI/Components/BaseComp.cs
+++ b/Lux.UI/Components/BaseComp.cs
@@ -19,7 +19,7 @@
///
protected string Traduci(string lemma)
{
- return VService.Traduci(lingua, lemma);
+ return VService.Traduci(lemma, lingua);
}
#endregion Protected Methods
diff --git a/Lux.UI/Lux.UI.csproj b/Lux.UI/Lux.UI.csproj
index 63be1cb9..4ea177bb 100644
--- a/Lux.UI/Lux.UI.csproj
+++ b/Lux.UI/Lux.UI.csproj
@@ -5,7 +5,7 @@
enable
enable
aspnet-Lux.UI-a758c101-a2f4-4e38-977d-1c4887dbbd50
- 1.1.2606.0311
+ 1.1.2606.0409
@@ -28,6 +28,7 @@
+
diff --git a/Lux.UI/Program.cs b/Lux.UI/Program.cs
index 5e1d4633..f34c728f 100644
--- a/Lux.UI/Program.cs
+++ b/Lux.UI/Program.cs
@@ -1,13 +1,11 @@
-using EgwCoreLib.Lux.Data;
using Lux.UI.Components;
using Lux.UI.Components.Account;
using Lux.UI.Data;
-using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Localization;
using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.FileProviders;
-using NLog;
using NLog.Targets;
using NLog.Web;
using OpenTelemetry.Resources;
@@ -15,6 +13,9 @@ using OpenTelemetry.Trace;
using Radzen;
using StackExchange.Redis;
using System.Globalization;
+using ZiggyCreatures.Caching.Fusion;
+using ZiggyCreatures.Caching.Fusion.Backplane.StackExchangeRedis;
+using ZiggyCreatures.Caching.Fusion.Serialization.NewtonsoftJson;
var builder = WebApplication.CreateBuilder(args);
// recupero env corrente
@@ -30,7 +31,21 @@ logger.Info($"Current ASPNETCORE_ENVIRONMENT: {env.EnvironmentName}");
// costruzione connectionMultiplexer redis...
string connStr = configuration.GetConnectionString("Redis") ?? "localhost";
-ConnectionMultiplexer redisConn = ConnectionMultiplexer.Connect(connStr);
+IConnectionMultiplexer redisMPlexer = ConnectionMultiplexer.Connect(connStr);
+
+// ✅ Distributed cache (necessario per FusionCache)
+builder.Services.AddStackExchangeRedisCache(options =>
+{
+ options.Configuration = connStr;
+});
+// ✅ FusionCache
+builder.Services.AddFusionCache()
+ .WithDistributedCache(sp => sp.GetRequiredService())
+ .WithSerializer(new FusionCacheNewtonsoftJsonSerializer())
+ .WithBackplane(new RedisBackplane(new RedisBackplaneOptions
+ {
+ ConnectionMultiplexerFactory = () => Task.FromResult(redisMPlexer)
+ }));
// ====================================================================
// Setup Tracing e Telemetria...
@@ -60,7 +75,7 @@ if (otelEnabled)
.AddSource("Lux.UI")
.AddAspNetCoreInstrumentation(options => { options.Filter = ctx => !ctx.Request.Path.StartsWithSegments("/health"); })
.AddEntityFrameworkCoreInstrumentation()
- .AddRedisInstrumentation(redisConn);
+ .AddRedisInstrumentation(redisMPlexer);
// ====================================================================
// ESPORTAZIONE DI RETE (Solo Livelli 1 e 2)
@@ -140,7 +155,7 @@ builder.Services.AddAuthentication(options =>
.AddIdentityCookies();
// registro connMultiplexer REDIS
-builder.Services.AddSingleton(redisConn);
+builder.Services.AddSingleton(redisMPlexer);
var conn = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
diff --git a/Resources/ChangeLog.html b/Resources/ChangeLog.html
index abebfbd9..2b235c07 100644
--- a/Resources/ChangeLog.html
+++ b/Resources/ChangeLog.html
@@ -1,6 +1,6 @@
LUX - Web Windows MES
- Versione: 1.1.2606.0311
+ Versione: 1.1.2606.0409
Note di rilascio:
-
diff --git a/Resources/VersNum.txt b/Resources/VersNum.txt
index 05dc50a7..9397ebfe 100644
--- a/Resources/VersNum.txt
+++ b/Resources/VersNum.txt
@@ -1 +1 @@
-1.1.2606.0311
+1.1.2606.0409
diff --git a/Resources/manifest.xml b/Resources/manifest.xml
index 56c535fd..44ee218f 100644
--- a/Resources/manifest.xml
+++ b/Resources/manifest.xml
@@ -1,6 +1,6 @@
-
- 1.1.2606.0311
+ 1.1.2606.0409
http://nexus.steamware.net/repository/SWS/GPW/stable/GPW.UI.zip
http://nexus.steamware.net/repository/SWS/GPW/stable/ChangeLog.html
false