From 4d79061eda569f86683aed1c419d867c9b4a8837 Mon Sep 17 00:00:00 2001 From: Samuele Locatelli Date: Thu, 4 Jun 2026 09:23:17 +0200 Subject: [PATCH] Update x prima integrazione vocabolario --- Directory.Packages.props | 4 + .../EgwCoreLib.Lux.Data.csproj | 3 + .../Services/Admin/IVocabolarioService.cs | 4 +- .../Services/Admin/VocabolarioService.cs | 304 ++++++++++-------- Lux.API/Lux.API.csproj | 2 +- Lux.API/Program.cs | 19 +- Lux.UI/Components/BaseComp.cs | 2 +- Lux.UI/Lux.UI.csproj | 3 +- Lux.UI/Program.cs | 27 +- Resources/ChangeLog.html | 2 +- Resources/VersNum.txt | 2 +- Resources/manifest.xml | 2 +- 12 files changed, 223 insertions(+), 151 deletions(-) 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