Fix servizi e repository vocabolario

This commit is contained in:
Samuele Locatelli
2026-06-04 11:42:35 +02:00
parent 07069ce151
commit bb358790ea
11 changed files with 280 additions and 154 deletions
@@ -11,7 +11,7 @@
Task<bool> AddAsync(VocabolarioModel entity);
/// <summary>
/// Clona un Vocabolario dalla linga originale a quella target
/// Clona un vocabolario data lingua origine/destinazione
/// </summary>
/// <param name="linguaOrig"></param>
/// <param name="linguaDest"></param>
@@ -46,11 +46,28 @@
Task<List<VocabolarioModel>> GetByLang(string lingua);
/// <summary>
/// Recuperoelenco lingue
/// Recupero elenco lingue
/// </summary>
/// <returns></returns>
Task<List<LinguaModel>> ListLingueAsync();
/// <summary>
/// Recupera un dizionario lemma->traduzione per una data lingua (sync, via CreateDbContext).
/// Usato per caricamenti sincroni da FusionCache factory.
/// </summary>
Dictionary<string, string> GetDizionarioLinguaSync(string lingua);
/// <summary>
/// Recupera un dizionario lemma->traduzione per una data lingua (async).
/// </summary>
Task<Dictionary<string, string>> GetDizionarioLinguaAsync(string lingua);
/// <summary>
/// Inserisce un record Vocabolario usando DbContext sincrono.
/// Usato per autogrowth del dizionario in Traduci().
/// </summary>
bool InsertSync(VocabolarioModel entity);
/// <summary>
/// Aggiorna un record Vocabolario esistente nel database.
/// </summary>
@@ -117,6 +117,32 @@
.ToListAsync();
}
/// <inheritdoc />
public Dictionary<string, string> GetDizionarioLinguaSync(string lingua)
{
using var dbCtx = _ctxFactory.CreateDbContext();
return dbCtx.DbSetVocabolario
.Where(x => x.Lingua == lingua)
.ToDictionary(d => d.Lemma, d => d.Traduzione);
}
/// <inheritdoc />
public async Task<Dictionary<string, string>> GetDizionarioLinguaAsync(string lingua)
{
await using var dbCtx = await CreateContextAsync();
return await dbCtx.DbSetVocabolario
.Where(x => x.Lingua == lingua)
.ToDictionaryAsync(d => d.Lemma, d => d.Traduzione);
}
/// <inheritdoc />
public bool InsertSync(VocabolarioModel entity)
{
using var dbCtx = _ctxFactory.CreateDbContext();
dbCtx.DbSetVocabolario.Add(entity);
return dbCtx.SaveChanges() > 0;
}
/// <inheritdoc />
public async Task<bool> UpdateAsync(VocabolarioModel entity)
{
@@ -30,6 +30,20 @@
/// <returns></returns>
Task<bool> FlushFusionCacheAsync();
/// <summary>
/// Cancellazione FusionCache dato tag
/// </summary>
/// <param name="tag"></param>
/// <returns></returns>
Task<bool> FlushFusionCacheAsync(string tag);
/// <summary>
/// Cancellazione FusionCache data lista tag
/// </summary>
/// <param name="listTags"></param>
/// <returns></returns>
Task<bool> FlushFusionCacheAsync(List<string> listTags);
/// <summary>
/// Recupera un Vocabolario per ID
/// </summary>
@@ -2,11 +2,6 @@
namespace EgwCoreLib.Lux.Data.Services.Admin
{
/// <summary>
/// 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.
/// </summary>
public class VocabolarioService : BaseServ, IVocabolarioService
{
#region Public Constructors
@@ -14,12 +9,13 @@ namespace EgwCoreLib.Lux.Data.Services.Admin
public VocabolarioService(
IConfiguration config,
IConnectionMultiplexer redis,
IFusionCache cache,
IDbContextFactory<DataLayerContext> factory) : base(config, redis)
IVocabolarioRepository repo,
IFusionCache cache) : base(config, redis)
{
_className = "Vocabolario";
slowLogThresh = config.GetValue<double>("ServerConf:slowLogThresh", 1);
_repo = repo;
_cache = cache;
_factory = factory;
}
#endregion Public Constructors
@@ -31,58 +27,27 @@ namespace EgwCoreLib.Lux.Data.Services.Admin
{
return await TraceAsync($"{_className}.Clone", async (activity) =>
{
var ctx = await _factory.CreateDbContextAsync();
var list = await ctx.DbSetVocabolario
.Where(x => x.Lingua == linguaOrig)
.ToListAsync();
bool result = await _repo.CloneAsync(linguaOrig, linguaDest);
if (!list.Any()) return false;
await using var tx = await ctx.Database.BeginTransactionAsync();
bool done = false;
try
if (result)
{
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();
await FlushFusionCacheAsync();
}
catch
{
await tx.RollbackAsync();
throw;
}
return done;
return result;
});
}
/// <inheritdoc />
public async Task<bool> DeleteAsync(VocabolarioModel rec2del)
public async Task<bool> DeleteAsync(VocabolarioModel model)
{
return await TraceAsync($"{_className}.Delete", async (activity) =>
{
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(model);
if (dbRec == null) return false;
ctx.DbSetVocabolario.Remove(dbRec);
bool success = await ctx.SaveChangesAsync() > 0;
if (success)
{
await FlushFusionCacheAsync();
}
return success;
});
}
@@ -94,6 +59,29 @@ namespace EgwCoreLib.Lux.Data.Services.Admin
return true;
}
/// <inheritdoc />
public async Task<bool> FlushFusionCacheAsync(string tag)
{
if (string.IsNullOrWhiteSpace(tag)) return false;
await _cache.RemoveByTagAsync(tag);
return true;
}
/// <inheritdoc />
public async Task<bool> FlushFusionCacheAsync(List<string> listTags)
{
if (listTags == null || listTags.Count == 0) return false;
// Generiamo i Task di rimozione ed eseguiamoli in parallelo su Redis/L1
var tasks = listTags
.Where(tag => !string.IsNullOrWhiteSpace(tag))
.Select(tag => _cache.RemoveByTagAsync(tag).AsTask());
await Task.WhenAll(tasks);
return true;
}
/// <inheritdoc />
public async Task<List<VocabolarioModel>> GetAllAsync()
{
@@ -101,11 +89,7 @@ namespace EgwCoreLib.Lux.Data.Services.Admin
{
return await GetOrSetCacheAsync(
$"{_redisBaseKey}:{_className}:ALL",
async () =>
{
var ctx = await _factory.CreateDbContextAsync();
return await ctx.DbSetVocabolario.ToListAsync();
}
async () => await _repo.GetAllAsync()
);
});
}
@@ -117,12 +101,7 @@ namespace EgwCoreLib.Lux.Data.Services.Admin
{
return await GetOrSetCacheAsync(
$"{_redisBaseKey}:{_className}:GetById:{lingua}:{lemma}",
async () =>
{
var ctx = await _factory.CreateDbContextAsync();
return await ctx.DbSetVocabolario
.FirstOrDefaultAsync(x => x.Lingua == lingua && x.Lemma == lemma);
}
async () => await _repo.GetByIdAsync(lingua, lemma)
);
});
}
@@ -130,15 +109,13 @@ namespace EgwCoreLib.Lux.Data.Services.Admin
/// <inheritdoc />
public async Task<List<VocabolarioModel>> GetByLang(string lingua)
{
// Fallback: DB + Redis cache
return await GetOrSetCacheAsync(
$"{_redisBaseKey}:{_className}:GetByLang:{lingua}",
async () =>
{
var ctx = await _factory.CreateDbContextAsync();
return await ctx.DbSetVocabolario.Where(o => o.Lingua == lingua).ToListAsync();
}
);
return await TraceAsync($"{_className}.GetByLang", async (activity) =>
{
return await GetOrSetCacheAsync(
$"{_redisBaseKey}:{_className}:GetByLang:{lingua}",
async () => await _repo.GetByLang(lingua)
);
});
}
/// <inheritdoc />
@@ -148,11 +125,7 @@ namespace EgwCoreLib.Lux.Data.Services.Admin
{
return await GetOrSetCacheAsync(
$"{_redisBaseKey}:{_className}:Lingue",
async () =>
{
var ctx = await _factory.CreateDbContextAsync();
return await ctx.DbSetLingua.ToListAsync();
}
async () => await _repo.ListLingueAsync()
);
});
}
@@ -166,49 +139,46 @@ namespace EgwCoreLib.Lux.Data.Services.Admin
string linguaKey = lingua.ToLowerInvariant().Trim();
string cacheKey = $"vocab:{linguaKey}";
var ctx = _factory.CreateDbContext();
// preparo opzioni
var cacheOptions = new FusionCacheEntryOptions()
.SetDuration(TimeSpan.FromMinutes(5))
.SetFailSafe(true, TimeSpan.FromMinutes(15));
// preparo tags
List<string> tagsList = new List<string>() { _className };
// 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.
// FusionCache gestisce lock + L1 memory + L2 redis
// Il factory usa repo.GetDizionarioLinguaSync (CreateDbContext sync)
var dizionarioLingua = _cache.GetOrSet<Dictionary<string, string>>(
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
_ => _repo.GetDizionarioLinguaSync(linguaKey),
options: cacheOptions,
tags: tagsList
);
// Ricerca O(1) nel dizionario in memoria
if (dizionarioLingua != null && dizionarioLingua.TryGetValue(lemma, out var traduzione))
{
return traduzione;
}
// Fallback: se la parola non è censita inserisce su DB
// Lemma non trovato: inserisco nel DB e aggiornero la cache
string answ = $"[[{lingua}_{lemma}]]";
var newRec = new VocabolarioModel()
{
Lingua = lingua,
Lemma = lemma,
Traduzione = answ
};
try
{
ctx.DbSetVocabolario.Add(newRec);
ctx.SaveChanges();
_repo.InsertSync(new VocabolarioModel
{
Lingua = lingua,
Lemma = lemma,
Traduzione = answ
});
// svuoto cache...
//_cache.Clear(allowFailSafe: false);
FlushFusionCache(_className);
}
catch (Exception exc)
catch
{
Log.Error(exc);
// Silenzio: il lemma non è stato inserito
}
// ...e restituisce il lemma originale (lingua + lemma)
return answ;
}
@@ -217,25 +187,27 @@ namespace EgwCoreLib.Lux.Data.Services.Admin
{
return await TraceAsync($"{_className}.Upsert", async (activity) =>
{
var ctx = await _factory.CreateDbContextAsync();
var currRec = await ctx.DbSetVocabolario
.FirstOrDefaultAsync(x => x.Lingua == upsRec.Lingua && x.Lemma == upsRec.Lemma);
var currRec = await _repo.GetByIdAsync(upsRec.Lingua, upsRec.Lemma);
string operation = "UPDATE";
bool success = false;
if (currRec != null)
{
ctx.Entry(currRec).CurrentValues.SetValues(upsRec);
success = await ctx.SaveChangesAsync() > 0;
success = await _repo.UpdateAsync(upsRec);
}
else
{
operation = "INSERT";
await ctx.DbSetVocabolario.AddAsync(upsRec);
success = await ctx.SaveChangesAsync() > 0;
success = await _repo.AddAsync(upsRec);
}
activity?.SetTag("db.operation", operation);
if (success)
{
await FlushFusionCacheAsync();
}
return success;
});
}
@@ -245,62 +217,122 @@ namespace EgwCoreLib.Lux.Data.Services.Admin
{
return await TraceAsync($"{_className}.UpsertMany", async (activity) =>
{
var ctx = await _factory.CreateDbContextAsync();
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();
var listExist = dbList
.Where(t => upsList.Any(d => d.Lingua == t.Lingua && d.Lemma == t.Lemma))
.ToList();
foreach (var dto in upsList)
{
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;
bool success = await _repo.UpsertManyAsync(upsList);
activity?.SetTag("db.operation", operation);
if (success)
{
await FlushFusionCacheAsync();
}
return success;
});
}
#endregion Public Methods
#region Protected Fields
/// <summary>
/// Soglia minima (ms) per log timing in console
/// </summary>
private double slowLogThresh = 0;
/// <summary>
/// Oggetto gestione FusionCache
/// Implementa gestione FusionCache+ tracking attività
/// - recupero cache da memoria o da obj esterno + cache memoria
/// - recupero da fetchFunc se mancasse + store in cache L1/L2
/// </summary>
protected readonly IFusionCache _cache;
/// <typeparam name="T"></typeparam>
/// <param name="cacheKey"></param>
/// <param name="fetchFunc"></param>
/// <param name="expiration"></param>
/// <returns></returns>
protected async Task<T> GetOrFetchAsync<T>(string operationName, string cacheKey, Func<Task<T>> fetchFunc, TimeSpan expiration, params string[] tagList)
{
using var activity = ActivitySource.StartActivity(operationName);
string source;
var tryGet = await _cache.TryGetAsync<T>(cacheKey);
if (tryGet.HasValue)
{
source = "MEMORY";
var result = tryGet.Value!;
#endregion Protected Fields
activity?.SetTag("data.source", source);
activity?.Stop();
// se supero la soglia loggo...
if (activity?.Duration.TotalMilliseconds > slowLogThresh)
{
LogTrace($"{operationName} | {source} | {activity?.Duration.TotalMilliseconds:F4} ms");
}
return result;
}
bool fromDb = false;
// cache in redis
var cacheOptions = new FusionCacheEntryOptions()
.SetDuration(expiration)
.SetFailSafe(true);
// cache in RAM per 1/3 del tempo x risparmiare risorse
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();
// switch log in base a source..
switch (source)
{
case "DB":
LogTrace($"{operationName} | {source} | {activity?.Duration.TotalMilliseconds:F4} ms", reqLevel: NLog.LogLevel.Info);
break;
case "REDIS":
LogTrace($"{operationName} | {source} | {activity?.Duration.TotalMilliseconds:F4} ms", reqLevel: NLog.LogLevel.Debug);
break;
default:
LogTrace($"{operationName} | {source} | {activity?.Duration.TotalMilliseconds:F4} ms");
break;
}
return final!;
}
#region Private Fields
private readonly IFusionCache _cache;
private readonly string _className;
private readonly IDbContextFactory<DataLayerContext> _factory;
private readonly IVocabolarioRepository _repo;
#endregion Private Fields
#region Private Methods
private bool FlushFusionCache()
{
_cache.Clear(allowFailSafe: false);
return true;
}
private bool FlushFusionCache(string tag)
{
if (string.IsNullOrWhiteSpace(tag)) return false;
_cache.RemoveByTag(tag);
return true;
}
#endregion Private Methods
}
}
+1 -1
View File
@@ -4,7 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<Version>1.1.2606.0410</Version>
<Version>1.1.2606.0411</Version>
</PropertyGroup>
<ItemGroup>
+1 -1
View File
@@ -5,7 +5,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>aspnet-Lux.UI-a758c101-a2f4-4e38-977d-1c4887dbbd50</UserSecretsId>
<Version>1.1.2606.0410</Version>
<Version>1.1.2606.0411</Version>
</PropertyGroup>
<ItemGroup>
+2 -1
View File
@@ -19,6 +19,7 @@
"ChannelPub": "EgwEngineInput",
"ChannelSub": "EgwEngineOutput",
"Prog.ApiUrl": "https://office.egalware.com/lux/srv/api",
"ReportUrl": "https://office.egalware.com/Lux/RepSrv"
"ReportUrl": "https://office.egalware.com/Lux/RepSrv",
"slowLogThresh": 100
}
}
+2 -1
View File
@@ -87,6 +87,7 @@
"BaseUrl": "/LUX/UI/",
"FileSharePath": "\\\\stor01\\TEAM DRIVES\\40_FileUpload\\LuxUploads",
"RedisBaseKey": "Lux",
"CleanupDayTTL": 180
"CleanupDayTTL": 180,
"slowLogThresh": 1
}
}
+28 -1
View File
@@ -1,6 +1,33 @@
<body>
<i>LUX - Web Windows MES</i>
<h4>Versione: 1.1.2606.0410</h4>
<h4>Versione: 1.1.2606.0411</h4>
<br /> Note di rilascio:
<ul>
<li>
<b>Ultime modifiche:</b>
<ul>{{LAST-CHANGES}}</ul>
</li>
<li>
<b>v.0.9.* &rarr;</b>
<ul>
<li>Versione preliminare</li>
<li>Release dotNet8</li>
<li>Integrazione EFCore</li>
</ul>
</li>
</ul>
<div>
<div style="float: left;">
<img src="logoEgalware.png" />
</div>
<div style="float: right;">
<a href="https://www.egalware.net/LUX" target="_blank">&copy; Egalware 2025+</a>
</div>
</div>
</body>
<body>
<i>LUX - Web Windows MES</i>
<h4>Versione: 1.1.2606.0411</h4>
<br /> Note di rilascio:
<ul>
<li>
+2 -1
View File
@@ -1 +1,2 @@
1.1.2606.0410
1.1.2606.0411
1.1.2606.0411
+8 -1
View File
@@ -1,6 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>1.1.2606.0410</version>
<version>1.1.2606.0411</version>
<url>http://nexus.steamware.net/repository/SWS/GPW/stable/GPW.UI.zip</url>
<changelog>http://nexus.steamware.net/repository/SWS/GPW/stable/ChangeLog.html</changelog>
<mandatory>false</mandatory>
</item>
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>1.1.2606.0411</version>
<url>http://nexus.steamware.net/repository/SWS/GPW/stable/GPW.UI.zip</url>
<changelog>http://nexus.steamware.net/repository/SWS/GPW/stable/ChangeLog.html</changelog>
<mandatory>false</mandatory>