Review cache con FusionCache

This commit is contained in:
Samuele Locatelli
2026-05-27 08:51:30 +02:00
parent 20a16471a9
commit 9e4594f8b4
9 changed files with 397 additions and 101 deletions
+1 -1
View File
@@ -24,7 +24,7 @@
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.36" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.36" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.36" />
<PackageVersion Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.25" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.1" />
<PackageVersion Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="6.0.17" />
<PackageVersion Include="MongoDB.Driver" Version="2.19.0" />
+23 -2
View File
@@ -342,9 +342,9 @@ namespace MP.Data.Controllers
/// Elenco valori ammessi x Stati commessa (es Yacht Baglietto)
/// </summary>
/// <returns></returns>
public List<ListValuesModel> AnagStatiComm()
public Task<List<ListValuesModel>> AnagStatiCommAsync()
{
return ListValuesFilt("PODL", "StatoComm");
return ListValuesFiltAsync("PODL", "StatoComm");
}
/// <summary>
@@ -1522,6 +1522,27 @@ namespace MP.Data.Controllers
return dbResult;
}
/// <summary>
/// Elenco valori ammessi x tabella/colonna Async
/// </summary>
/// <param name="tabName"></param>
/// <param name="fieldName"></param>
/// <returns></returns>
public async Task<List<ListValuesModel>> ListValuesFiltAsync(string tabName, string fieldName)
{
List<ListValuesModel> dbResult = new List<ListValuesModel>();
using (var dbCtx = new MoonProContext(options))
{
dbResult = await dbCtx
.DbSetListValues
.Where(x => x.TableName == tabName && x.FieldName == fieldName)
.AsNoTracking()
.OrderBy(x => x.ordinal)
.ToListAsync();
}
return dbResult;
}
/// <summary>
/// Elenco Macchine dato operatore secondo gruppi (macchine/operatore)
/// </summary>
+342 -90
View File
@@ -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)
+5 -1
View File
@@ -5,7 +5,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>MP.SPEC</RootNamespace>
<Version>8.16.2605.2619</Version>
<Version>8.16.2605.2708</Version>
<UserSecretsId>1800a78a-6ff1-40f9-b490-87fb8bfc1394</UserSecretsId>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>
@@ -42,6 +42,7 @@
<PackageReference Include="Microsoft.AspNetCore.Authentication.Negotiate" />
<PackageReference Include="Microsoft.AspNetCore.Components" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="NLog.Targets.OpenTelemetryProtocol" />
<PackageReference Include="NLog.Web.AspNetCore" />
@@ -52,6 +53,9 @@
<PackageReference Include="OpenTelemetry.Instrumentation.SqlClient" />
<PackageReference Include="OpenTelemetry.Instrumentation.StackExchangeRedis" />
<PackageReference Include="System.Text.Encodings.Web" />
<PackageReference Include="ZiggyCreatures.FusionCache" />
<PackageReference Include="ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis" />
<PackageReference Include="ZiggyCreatures.FusionCache.Serialization.NewtonsoftJson" />
</ItemGroup>
<ItemGroup>
+21 -3
View File
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Authentication.Negotiate;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.FileProviders;
using MP.AppAuth.Services;
using MP.Data.Services;
@@ -12,7 +13,9 @@ using NLog.Web;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using StackExchange.Redis;
using ZiggyCreatures.Caching.Fusion;
using ZiggyCreatures.Caching.Fusion.Backplane.StackExchangeRedis;
using ZiggyCreatures.Caching.Fusion.Serialization.NewtonsoftJson;
var builder = WebApplication.CreateBuilder(args);
@@ -30,7 +33,7 @@ string connStringRedis = configuration.GetConnectionString("Redis") ?? "localhos
//string connStringRedis = ConfMan.GetConnectionString("RedisAdmin");
string redisSrvAddr = connStringRedis.Substring(0, connStringRedis.IndexOf(":"));
// avvio oggetto shared x redis...
var redisMultiplexer = ConnectionMultiplexer.Connect(connStringRedis);
IConnectionMultiplexer redisMultiplexer = ConnectionMultiplexer.Connect(connStringRedis);
// ====================================================================
@@ -135,9 +138,24 @@ builder.Services.AddRazorComponents()
builder.Services.AddRazorPages();
// memory + redis preliminare
builder.Services.AddMemoryCache();
builder.Services.AddSingleton<IConnectionMultiplexer>(redisMultiplexer);
// ✅ Distributed cache (necessario per FusionCache)
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = connStringRedis;
});
// ✅ FusionCache
builder.Services.AddFusionCache()
.WithDistributedCache(sp => sp.GetRequiredService<IDistributedCache>())
.WithSerializer(new FusionCacheNewtonsoftJsonSerializer())
.WithBackplane(new RedisBackplane(new RedisBackplaneOptions
{
ConnectionMultiplexerFactory = () => Task.FromResult(redisMultiplexer)
}));
// altri servizi
builder.Services.AddSingleton<MpDataService>();
builder.Services.AddSingleton<ListSelectDataSrv>();
+1 -1
View File
@@ -1,6 +1,6 @@
<body>
<i>Modulo MAPOSPEC </i>
<h4>Versione: 8.16.2605.2619</h4>
<h4>Versione: 8.16.2605.2708</h4>
<br /> Note di rilascio:
<ul>
<li>
+1 -1
View File
@@ -1 +1 @@
8.16.2605.2619
8.16.2605.2708
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<item>
<version>8.16.2605.2619</version>
<version>8.16.2605.2708</version>
<url>https://nexus.steamware.net/repository/SWS/MP-SPEC/stable/LAST/MP.SPEC.zip</url>
<changelog>https://nexus.steamware.net/repository/SWS/MP-SPEC/stable/LAST/ChangeLog.html</changelog>
<mandatory>false</mandatory>
+2 -1
View File
@@ -2,7 +2,8 @@
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
"Microsoft.AspNetCore": "Warning",
"ZiggyCreatures.Caching.Fusion": "Warning"
}
},
"NLog": {