Review cache con FusionCache
This commit is contained in:
+342
-90
@@ -1,6 +1,5 @@
|
||||
using EgwCoreLib.Utils;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using MP.Core.Conf;
|
||||
using MP.Core.DTO;
|
||||
using MP.Core.Objects;
|
||||
@@ -14,6 +13,7 @@ using NLog;
|
||||
using StackExchange.Redis;
|
||||
using System.Data;
|
||||
using System.Diagnostics;
|
||||
using ZiggyCreatures.Caching.Fusion;
|
||||
|
||||
namespace MP.SPEC.Data
|
||||
{
|
||||
@@ -21,11 +21,11 @@ namespace MP.SPEC.Data
|
||||
{
|
||||
#region Public Constructors
|
||||
|
||||
public MpDataService(IConfiguration configuration, IMemoryCache memoryCache)
|
||||
public MpDataService(IConfiguration configuration, IFusionCache cache)
|
||||
{
|
||||
// salvataggio oggetti
|
||||
_configuration = configuration;
|
||||
_memoryCache = memoryCache;
|
||||
_cache = cache;
|
||||
// Verifica conf trace...
|
||||
traceEnabled = _configuration.GetValue<bool>("Otel:EnableTracing", false);
|
||||
Log.Info($"MpDataService | INIT | Trace enabled: {traceEnabled}");
|
||||
@@ -300,15 +300,16 @@ namespace MP.SPEC.Data
|
||||
LogTrace($"AnagKeyValGetAll Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<List<ListValuesModel>> AnagStatiComm()
|
||||
{
|
||||
return await GetOrCreateCachedAsync(
|
||||
operationName: "AnagStatiComm",
|
||||
memKey: "ANAG_STATI_COMM_MEM",
|
||||
redisKey: Utils.redisStatoCom,
|
||||
memoryTtl: TimeSpan.FromMinutes(5),
|
||||
dbFactory: () => Task.FromResult(dbController.AnagStatiComm() ?? new List<ListValuesModel>())
|
||||
);
|
||||
return await GetOrFetchAsync(
|
||||
operationName: "AnagStatiCommAsync",
|
||||
cacheKey: Utils.redisStatoCom,
|
||||
expiration: TimeSpan.FromMinutes(5),
|
||||
fetchFunc: async () =>
|
||||
await dbController.AnagStatiCommAsync() ?? new List<ListValuesModel>()
|
||||
);
|
||||
}
|
||||
|
||||
#if false
|
||||
@@ -345,7 +346,7 @@ namespace MP.SPEC.Data
|
||||
|
||||
public async Task<List<ListValuesModel>> AnagTipoArtLV()
|
||||
{
|
||||
using var activity = ActivitySource.StartActivity("AnagStatiComm");
|
||||
using var activity = ActivitySource.StartActivity("AnagStatiCommAsync");
|
||||
string source = "DB";
|
||||
List<ListValuesModel>? result = new List<ListValuesModel>();
|
||||
// cerco in redis...
|
||||
@@ -438,16 +439,28 @@ namespace MP.SPEC.Data
|
||||
string redisKey = $"{Utils.redisArtList}:{azienda}:Tipo:{sKey}";
|
||||
string memKey = $"MEM:{redisKey}";
|
||||
|
||||
return await GetOrCreateCachedAsync(
|
||||
operationName: "ArticoliGetByTipoAsync",
|
||||
memKey: memKey,
|
||||
redisKey: redisKey,
|
||||
// ✅ TTL lungo (dato abbastanza statico)
|
||||
memoryTtl: TimeSpan.FromMinutes(5),
|
||||
dbFactory: async () =>
|
||||
await dbController.ArticoliGetByTipoAsync(tipo, azienda)
|
||||
return await GetOrFetchAsync(
|
||||
operationName: "ArticoliGetByTipoAsync",
|
||||
cacheKey: redisKey,
|
||||
expiration: TimeSpan.FromMinutes(2),
|
||||
fetchFunc: async () =>
|
||||
await dbController.ArticoliGetByTipoAsync(tipo, azienda)
|
||||
?? new List<AnagArticoliModel>()
|
||||
);
|
||||
);
|
||||
|
||||
|
||||
#if false
|
||||
return await GetOrCreateCachedAsync(
|
||||
operationName: "ArticoliGetByTipoAsync",
|
||||
memKey: memKey,
|
||||
redisKey: redisKey,
|
||||
// ✅ TTL lungo (dato abbastanza statico)
|
||||
memoryTtl: TimeSpan.FromMinutes(5),
|
||||
dbFactory: async () =>
|
||||
await dbController.ArticoliGetByTipoAsync(tipo, azienda)
|
||||
?? new List<AnagArticoliModel>()
|
||||
);
|
||||
#endif
|
||||
}
|
||||
#if false
|
||||
public async Task<List<AnagArticoliModel>> ArticoliGetByTipoAsync(string tipo, string azienda = "*")
|
||||
@@ -496,18 +509,161 @@ namespace MP.SPEC.Data
|
||||
string memKey = $"ART_SEARCH_MEM:{azienda}:{sKey}:{numRecord}";
|
||||
string redisKey = $"{Utils.redisArtList}:{azienda}:{sKey}:{numRecord}";
|
||||
|
||||
return await GetOrCreateCachedAsync(
|
||||
operationName: "ArticoliGetSearchAsync",
|
||||
memKey: memKey,
|
||||
redisKey: redisKey,
|
||||
memoryTtl: TimeSpan.FromMinutes(2),
|
||||
dbFactory: async () =>
|
||||
await dbController.ArticoliGetSearchAsync(numRecord, azienda, searchVal)
|
||||
return await GetOrFetchAsync(
|
||||
operationName: "ArticoliGetSearchAsync",
|
||||
cacheKey: redisKey,
|
||||
expiration: TimeSpan.FromMinutes(2),
|
||||
fetchFunc: async () =>
|
||||
await dbController.ArticoliGetSearchAsync(numRecord, azienda, searchVal)
|
||||
?? new List<AnagArticoliModel>()
|
||||
);
|
||||
);
|
||||
|
||||
#if false
|
||||
return await GetOrCreateCachedAsync(
|
||||
operationName: "ArticoliGetSearchAsync",
|
||||
memKey: memKey,
|
||||
redisKey: redisKey,
|
||||
memoryTtl: TimeSpan.FromMinutes(2),
|
||||
dbFactory: async () =>
|
||||
await dbController.ArticoliGetSearchAsync(numRecord, azienda, searchVal)
|
||||
?? new List<AnagArticoliModel>()
|
||||
);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implementa gestione recupero cache da memoria o da obj esterno + cache memoria + tracking attività
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="cacheKey"></param>
|
||||
/// <param name="fetchFunc"></param>
|
||||
/// <param name="expiration"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<T> GetOrFetchAsync<T>(string operationName, string cacheKey, Func<Task<T>> fetchFunc, TimeSpan expiration, params string[] tags)
|
||||
{
|
||||
using var activity = ActivitySource.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);
|
||||
LogTrace($"{operationName} | {source} | {activity?.Duration.TotalMilliseconds:F4} ms");
|
||||
return result;
|
||||
}
|
||||
bool fromDb = false;
|
||||
var final = await _cache.GetOrSetAsync<T>(
|
||||
cacheKey,
|
||||
async _ =>
|
||||
{
|
||||
fromDb = true;
|
||||
return await fetchFunc();
|
||||
},
|
||||
opt =>
|
||||
{
|
||||
opt.SetDuration(expiration)
|
||||
.SetFailSafe(true);
|
||||
|
||||
//if (tags != null && tags.Length > 0)
|
||||
// opt.SetTags(tags);
|
||||
|
||||
});
|
||||
|
||||
source = fromDb ? "DB" : "REDIS";
|
||||
activity?.SetTag("data.source", source);
|
||||
LogTrace($"{operationName} | {source} | {activity?.Duration.TotalMilliseconds:F4} ms");
|
||||
return final!;
|
||||
}
|
||||
|
||||
#if false
|
||||
private async Task<T> GetOrFetchAsync<T>(string operationName, string cacheKey, Func<Task<T>> fetchFunc, TimeSpan expiration)
|
||||
{
|
||||
using var activity = ActivitySource.StartActivity(operationName);
|
||||
|
||||
string source;
|
||||
T result;
|
||||
|
||||
// ✅ 1. Tenta MEMORY / DISTRIBUTED (senza factory)
|
||||
var memTry = await _cache.TryGetAsync<T>(cacheKey);
|
||||
|
||||
if (memTry.HasValue)
|
||||
{
|
||||
result = memTry.Value!;
|
||||
source = "MEMORY";
|
||||
}
|
||||
else
|
||||
{
|
||||
bool fromDb = false;
|
||||
|
||||
// ✅ 2. fallback con factory
|
||||
result = await _cache.GetOrSetAsync<T>(
|
||||
cacheKey,
|
||||
async _ =>
|
||||
{
|
||||
fromDb = true;
|
||||
return await fetchFunc();
|
||||
},
|
||||
options => options.SetDuration(expiration)
|
||||
)!;
|
||||
|
||||
source = fromDb ? "DB" : "REDIS";
|
||||
}
|
||||
|
||||
// ✅ tracing e log
|
||||
activity?.SetTag("data.source", source);
|
||||
|
||||
if (result is System.Collections.ICollection coll)
|
||||
activity?.SetTag("result.count", coll.Count);
|
||||
else
|
||||
activity?.SetTag("result.count", result != null ? 1 : 0);
|
||||
|
||||
LogTrace($"{operationName} | {source} | {activity?.Duration.TotalMilliseconds:F4} ms");
|
||||
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Cancellazione FusionCache (totale)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> FlushCacheAsync()
|
||||
{
|
||||
bool fatto = false;
|
||||
await _cache.ClearAsync();
|
||||
fatto = true;
|
||||
return fatto;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancellazione FusionCache dato elenco tags
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> FlushCacheByTagsAsync(List<string> listTags)
|
||||
{
|
||||
bool fatto = false;
|
||||
foreach (var item in listTags)
|
||||
{
|
||||
await _cache.RemoveByTagAsync(item);
|
||||
}
|
||||
fatto = true;
|
||||
return fatto;
|
||||
}
|
||||
/// <summary>
|
||||
/// Cancellazione FusionCache dato singolo tag
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> FlushCacheByTagAsync(string tag)
|
||||
{
|
||||
bool fatto = false;
|
||||
await _cache.RemoveByTagAsync(tag);
|
||||
fatto = true;
|
||||
return fatto;
|
||||
}
|
||||
|
||||
#if false
|
||||
/// <summary>
|
||||
/// Helper per gestione cache a 3 livelli: MEMORY, REDIS e DB con opzioni
|
||||
/// </summary>
|
||||
@@ -527,12 +683,20 @@ namespace MP.SPEC.Data
|
||||
string source = "NA";
|
||||
T result;
|
||||
|
||||
string groupKey = memKey.Split(':')[0]; // es: "MACCHINE_MEM"
|
||||
|
||||
_memoryKeys.AddOrUpdate(
|
||||
groupKey,
|
||||
_ => new HashSet<string> { memKey },
|
||||
(_, set) => { set.Add(memKey); return set; }
|
||||
);
|
||||
|
||||
// ✅ default serializer (fallback)
|
||||
serialize ??= (obj) => JsonConvert.SerializeObject(obj);
|
||||
deserialize ??= (str) => JsonConvert.DeserializeObject<T>(str)!;
|
||||
|
||||
// ✅ 1. MEMORY
|
||||
if (_memoryCache.TryGetValue(memKey, out T cached))
|
||||
if (_cache.TryGetValue(memKey, out T cached))
|
||||
{
|
||||
result = cached;
|
||||
source = "MEMORY";
|
||||
@@ -540,24 +704,17 @@ namespace MP.SPEC.Data
|
||||
else
|
||||
{
|
||||
// ✅ 2. MISS → factory
|
||||
result = await _memoryCache.GetOrCreateAsync(memKey, async entry =>
|
||||
result = await _cache.GetOrCreateAsync(memKey, async entry =>
|
||||
{
|
||||
entry.AbsoluteExpirationRelativeToNow = memoryTtl;
|
||||
|
||||
// 👉 REDIS
|
||||
try
|
||||
{
|
||||
var rawData = await redisDb.StringGetAsync(redisKey);
|
||||
var rawData = await redisDb.StringGetAsync(redisKey);
|
||||
|
||||
if (rawData.HasValue)
|
||||
{
|
||||
source = "REDIS";
|
||||
return deserialize(rawData!);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
if (rawData.HasValue)
|
||||
{
|
||||
LogTrace($"Redis error on {operationName}: {ex.Message}");
|
||||
source = "REDIS";
|
||||
return deserialize(rawData!);
|
||||
}
|
||||
|
||||
// 👉 DB
|
||||
@@ -567,18 +724,11 @@ namespace MP.SPEC.Data
|
||||
|
||||
var safeResult = dbResult == null ? default! : dbResult;
|
||||
|
||||
try
|
||||
{
|
||||
await redisDb.StringSetAsync(
|
||||
redisKey,
|
||||
serialize(safeResult),
|
||||
getRandTOut(redisLongTimeCache)
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogTrace($"Redis SET error on {operationName}: {ex.Message}");
|
||||
}
|
||||
await redisDb.StringSetAsync(
|
||||
redisKey,
|
||||
serialize(safeResult),
|
||||
getRandTOut(redisLongTimeCache)
|
||||
);
|
||||
|
||||
return safeResult;
|
||||
})!;
|
||||
@@ -595,8 +745,64 @@ namespace MP.SPEC.Data
|
||||
LogTrace($"{operationName} | {source} | {activity?.Duration.TotalMilliseconds}ms");
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invalidazione di una chiave in memoria e Redis
|
||||
/// </summary>
|
||||
/// <param name="memKey"></param>
|
||||
/// <param name="redisKey"></param>
|
||||
/// <returns></returns>
|
||||
public async Task InvalidateCacheAsync(string memKey, string redisKey)
|
||||
{
|
||||
// ✅ memoria
|
||||
_cache.Remove(memKey);
|
||||
|
||||
// ✅ redis
|
||||
await redisDb.KeyDeleteAsync(redisKey);
|
||||
|
||||
LogTrace($"Cache invalidated | {memKey}");
|
||||
}
|
||||
private readonly ConcurrentDictionary<string, HashSet<string>> _memoryKeys = new();
|
||||
/// <summary>
|
||||
/// Invalidazione cache da pattern
|
||||
/// </summary>
|
||||
/// <param name="pattern"></param>
|
||||
/// <returns></returns>
|
||||
public async Task InvalidateCacheByPatternAsync(string pattern)
|
||||
{
|
||||
// ✅ MEMORY (pattern match semplice)
|
||||
var keysToRemove = _memoryKeys.Keys
|
||||
.Where(k => k.Contains(pattern.Replace("*", "")))
|
||||
.ToList();
|
||||
|
||||
foreach (var key in keysToRemove)
|
||||
{
|
||||
if (_memoryKeys.TryRemove(key, out var subKeys))
|
||||
{
|
||||
foreach (var k in subKeys)
|
||||
{
|
||||
_cache.Remove(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ REDIS
|
||||
var masterEndpoint = redisConn.GetEndPoints()
|
||||
.Where(ep => redisConn.GetServer(ep).IsConnected && !redisConn.GetServer(ep).IsReplica)
|
||||
.FirstOrDefault();
|
||||
var server = redisConn.GetServer(masterEndpoint);
|
||||
|
||||
var redisKeys = server.Keys(pattern: pattern);
|
||||
|
||||
foreach (var rk in redisKeys)
|
||||
{
|
||||
await redisDb.KeyDeleteAsync(rk);
|
||||
}
|
||||
|
||||
LogTrace($"Cache invalidated by pattern | {pattern}");
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Aggiornamento record selezionato
|
||||
@@ -1798,7 +2004,7 @@ namespace MP.SPEC.Data
|
||||
LogTrace($"MacchineGetFilt | Read from {source}: {activity?.Duration.TotalMilliseconds}ms");
|
||||
return result;
|
||||
}
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly IFusionCache _cache;
|
||||
/// <summary>
|
||||
/// Elenco di tutte le macchine filtrate x gruppo
|
||||
/// </summary>
|
||||
@@ -1811,16 +2017,27 @@ namespace MP.SPEC.Data
|
||||
string redisKey = $"{Utils.redisMacList}:{keyGrp}";
|
||||
string memKey = $"MACCHINE_MEM:{keyGrp}";
|
||||
|
||||
return await GetOrCreateCachedAsync(
|
||||
operationName: "MacchineGetFiltAsync",
|
||||
memKey: memKey,
|
||||
redisKey: redisKey,
|
||||
// ✅ TTL coerente con il tuo requisito (prima avevi 1 minuto)
|
||||
memoryTtl: TimeSpan.FromMinutes(1),
|
||||
dbFactory: async () =>
|
||||
await dbController.MacchineGetFiltAsync(codGruppo)
|
||||
return await GetOrFetchAsync(
|
||||
operationName: "MacchineGetFiltAsync",
|
||||
cacheKey: redisKey,
|
||||
expiration: TimeSpan.FromMinutes(5),
|
||||
fetchFunc: async () =>
|
||||
await dbController.MacchineGetFiltAsync(codGruppo)
|
||||
?? new List<MacchineModel>()
|
||||
);
|
||||
);
|
||||
|
||||
#if false
|
||||
return await GetOrCreateCachedAsync(
|
||||
operationName: "MacchineGetFiltAsync",
|
||||
memKey: memKey,
|
||||
redisKey: redisKey,
|
||||
// ✅ TTL coerente con il tuo requisito (prima avevi 1 minuto)
|
||||
memoryTtl: TimeSpan.FromMinutes(1),
|
||||
dbFactory: async () =>
|
||||
await dbController.MacchineGetFiltAsync(codGruppo)
|
||||
?? new List<MacchineModel>()
|
||||
);
|
||||
#endif
|
||||
}
|
||||
#if false
|
||||
public async Task<List<MacchineModel>> MacchineGetFiltAsync(string codGruppo)
|
||||
@@ -2196,23 +2413,41 @@ namespace MP.SPEC.Data
|
||||
string redisKey = Utils.redisOdlCurrByMac;
|
||||
string memKey = $"MEM:{redisKey}";
|
||||
|
||||
return await GetOrCreateCachedAsync(
|
||||
operationName: "OdlGetCurrentAsync",
|
||||
memKey: memKey,
|
||||
redisKey: redisKey,
|
||||
// ✅ TTL molto corto (come avevi: 3 secondi)
|
||||
memoryTtl: TimeSpan.FromSeconds(3),
|
||||
dbFactory: async () =>
|
||||
{
|
||||
var rawData = await dbController.OdlGetCurrentAsync();
|
||||
var dbResult = rawData
|
||||
.Select(x => x.IdxMacchina)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
return await GetOrFetchAsync(
|
||||
operationName: "OdlGetCurrentAsync",
|
||||
cacheKey: redisKey,
|
||||
expiration: TimeSpan.FromSeconds(3),
|
||||
fetchFunc: async () =>
|
||||
{
|
||||
var rawData = await dbController.OdlGetCurrentAsync();
|
||||
var dbResult = rawData
|
||||
.Select(x => x.IdxMacchina)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
return dbResult ?? new List<string>();
|
||||
}
|
||||
);
|
||||
return dbResult ?? new List<string>();
|
||||
}
|
||||
);
|
||||
|
||||
#if false
|
||||
return await GetOrCreateCachedAsync(
|
||||
operationName: "OdlGetCurrentAsync",
|
||||
memKey: memKey,
|
||||
redisKey: redisKey,
|
||||
// ✅ TTL molto corto (come avevi: 3 secondi)
|
||||
memoryTtl: TimeSpan.FromSeconds(3),
|
||||
dbFactory: async () =>
|
||||
{
|
||||
var rawData = await dbController.OdlGetCurrentAsync();
|
||||
var dbResult = rawData
|
||||
.Select(x => x.IdxMacchina)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
return dbResult ?? new List<string>();
|
||||
}
|
||||
);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if false
|
||||
@@ -2658,18 +2893,16 @@ namespace MP.SPEC.Data
|
||||
/// <returns></returns>
|
||||
public async Task<List<PODLExpModel>> POdlToKitListGetFiltAsync(bool lanciato, string keyRichPart, string idxMacchina, string codGruppo, DateTime startDate, DateTime endDate)
|
||||
{
|
||||
string currKey = $"{Utils.redisPOdlList}_kit:{codGruppo}:{idxMacchina}:{keyRichPart}:{lanciato}:{startDate:yyyyMMdd_HHmmss}:{endDate:yyyyMMdd_HHmmss}";
|
||||
string redisKey = $"{Utils.redisPOdlList}_kit:{codGruppo}:{idxMacchina}:{keyRichPart}:{lanciato}:{startDate:yyyyMMdd_HHmmss}:{endDate:yyyyMMdd_HHmmss}";
|
||||
|
||||
// ✅ stessa chiave per memoria (puoi anche prefissare)
|
||||
string memKey = $"MEM:{currKey}";
|
||||
string memKey = $"MEM:{redisKey}";
|
||||
|
||||
return await GetOrCreateCachedAsync(
|
||||
operationName: "POdlToKitListGetFiltAsync",
|
||||
memKey: memKey,
|
||||
redisKey: currKey,
|
||||
// ✅ TTL RAM breve (coerente con redisShortTimeCache)
|
||||
memoryTtl: TimeSpan.FromSeconds(redisShortTimeCache),
|
||||
dbFactory: async () =>
|
||||
return await GetOrFetchAsync(
|
||||
operationName: "POdlToKitListGetFiltAsync",
|
||||
cacheKey: redisKey,
|
||||
expiration: TimeSpan.FromSeconds(redisShortTimeCache),
|
||||
fetchFunc: async () =>
|
||||
await dbController.ListPODL_KitFiltAsync(
|
||||
lanciato,
|
||||
keyRichPart,
|
||||
@@ -2678,7 +2911,26 @@ namespace MP.SPEC.Data
|
||||
startDate,
|
||||
endDate
|
||||
) ?? new List<PODLExpModel>()
|
||||
);
|
||||
);
|
||||
|
||||
#if false
|
||||
return await GetOrCreateCachedAsync(
|
||||
operationName: "POdlToKitListGetFiltAsync",
|
||||
memKey: memKey,
|
||||
redisKey: redisKey,
|
||||
// ✅ TTL RAM breve (coerente con redisShortTimeCache)
|
||||
memoryTtl: TimeSpan.FromSeconds(redisShortTimeCache),
|
||||
dbFactory: async () =>
|
||||
await dbController.ListPODL_KitFiltAsync(
|
||||
lanciato,
|
||||
keyRichPart,
|
||||
idxMacchina,
|
||||
codGruppo,
|
||||
startDate,
|
||||
endDate
|
||||
) ?? new List<PODLExpModel>()
|
||||
);
|
||||
#endif
|
||||
}
|
||||
#if false
|
||||
public async Task<List<PODLExpModel>> POdlToKitListGetFiltAsync(bool lanciato, string keyRichPart, string idxMacchina, string codGruppo, DateTime startDate, DateTime endDate)
|
||||
|
||||
Reference in New Issue
Block a user