diff --git a/MP-RIOC/MP-RIOC.slnx b/MP-RIOC/MP-RIOC.slnx index 455ea819..c16a708c 100644 --- a/MP-RIOC/MP-RIOC.slnx +++ b/MP-RIOC/MP-RIOC.slnx @@ -1,4 +1,5 @@ + diff --git a/MP-RIOC/MP.RIOC.csproj b/MP-RIOC/MP.RIOC.csproj index 0c63a52d..e2cd4295 100644 --- a/MP-RIOC/MP.RIOC.csproj +++ b/MP-RIOC/MP.RIOC.csproj @@ -4,14 +4,10 @@ net8.0 enable enable - MP_RIOC + MP.RIOC 8.16.2605.808 - - - - @@ -25,10 +21,7 @@ - - - - + diff --git a/MP-RIOC/Program.cs b/MP-RIOC/Program.cs index 573ef479..c532a415 100644 --- a/MP-RIOC/Program.cs +++ b/MP-RIOC/Program.cs @@ -1,4 +1,5 @@ using MP.RIOC.Services; +using MP.RIOC.Services; using NLog; using NLog.Web; using StackExchange.Redis; @@ -54,6 +55,9 @@ builder.Services.AddSingleton(redisMux); // Registrazione dei servizi custom builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddHostedService(); +builder.Services.AddHostedService(); +builder.Services.AddSingleton(); logger.Info("Standard service configured"); diff --git a/MP-RIOC/RedisScript/RedisUpdateScript_v5.lua b/MP-RIOC/RedisScript/RedisUpdateScript_v5.lua new file mode 100644 index 00000000..1b4a2477 --- /dev/null +++ b/MP-RIOC/RedisScript/RedisUpdateScript_v5.lua @@ -0,0 +1,33 @@ +-- RedisUpdateScript_v5 +local key = KEYS[1] +local countInc = tonumber(ARGV[1]) or 0 +local totalMsInc = tonumber(ARGV[2]) or 0 +local newMax = tonumber(ARGV[3]) +local newMin = tonumber(ARGV[4]) +local sentinel = tonumber(ARGV[5]) + +-- Incrementi base +redis.call('HINCRBY', key, 'count', countInc) +redis.call('HINCRBYFLOAT', key, 'totalMs', totalMsInc) + +-- MAX +local currentMaxStr = redis.call('HGET', key, 'maxMs') +local currentMax = tonumber(currentMaxStr) + +if newMax ~= nil and newMax < sentinel then + if currentMax == nil or newMax > currentMax then + redis.call('HSET', key, 'maxMs', newMax) + end +end + +-- MIN +local currentMinStr = redis.call('HGET', key, 'minMs') +local currentMin = tonumber(currentMinStr) + +if newMin ~= nil and newMin < sentinel then + if currentMin == nil or newMin < currentMin then + redis.call('HSET', key, 'minMs', newMin) + end +end + +return 1 diff --git a/MP-RIOC/RedisScript/RedisUpdateScript_v6.lua b/MP-RIOC/RedisScript/RedisUpdateScript_v6.lua new file mode 100644 index 00000000..230ed7f9 --- /dev/null +++ b/MP-RIOC/RedisScript/RedisUpdateScript_v6.lua @@ -0,0 +1,33 @@ +-- RedisUpdateScript_v6 +local key = KEYS[1] +local countInc = tonumber(ARGV[1]) or 0 +local totalMsInc = tonumber(ARGV[2]) or 0 +local newMax = tonumber(ARGV[3]) +local newMin = tonumber(ARGV[4]) +local sentinel = tonumber(ARGV[5]) + +-- Incrementi +redis.call('HINCRBY', key, 'count', countInc) +redis.call('HINCRBYFLOAT', key, 'totalMs', totalMsInc) + +-- MAX +local currentMaxStr = redis.call('HGET', key, 'maxMs') +local currentMax = tonumber(currentMaxStr) + +if newMax ~= nil and newMax < sentinel then + if currentMax == nil or newMax > currentMax then + redis.call('HSET', key, 'maxMs', tostring(newMax)) + end +end + +-- MIN +local currentMinStr = redis.call('HGET', key, 'minMs') +local currentMin = tonumber(currentMinStr) + +if newMin ~= nil and newMin < sentinel then + if currentMin == nil or newMin < currentMin then + redis.call('HSET', key, 'minMs', tostring(newMin)) + end +end + +return 1 diff --git a/MP-RIOC/Services/LuaScriptProvider.cs b/MP-RIOC/Services/LuaScriptProvider.cs new file mode 100644 index 00000000..41951353 --- /dev/null +++ b/MP-RIOC/Services/LuaScriptProvider.cs @@ -0,0 +1,42 @@ +using Microsoft.Extensions.Options; +using MP.Core.Conf; + +namespace MP.RIOC.Services +{ + public sealed class LuaScriptProvider + { + private readonly Dictionary _scripts; + + public IReadOnlyDictionary Scripts => _scripts; + + public LuaScriptProvider( + IOptions cfg, + IWebHostEnvironment env) + { + _scripts = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var kv in cfg.Value.Scripts) + { + var name = kv.Key; + var relativePath = kv.Value; + + var fullPath = Path.Combine(env.ContentRootPath, relativePath); + + if (!File.Exists(fullPath)) + throw new FileNotFoundException($"Script Lua non trovato: {fullPath}"); + + var content = File.ReadAllText(fullPath); + + _scripts[name] = content; + } + } + + public string Get(string name) + { + if (!_scripts.TryGetValue(name, out var script)) + throw new KeyNotFoundException($"Script Lua '{name}' non trovato."); + + return script; + } + } +} diff --git a/MP-RIOC/Services/MetricsCalcService.cs b/MP-RIOC/Services/MetricsCalcService.cs new file mode 100644 index 00000000..1accf3a5 --- /dev/null +++ b/MP-RIOC/Services/MetricsCalcService.cs @@ -0,0 +1,224 @@ +using MP.RIOC.Services; +using NLog; +using StackExchange.Redis; +using System.Globalization; + +namespace MP.RIOC.Services +{ + public class MetricsCalcService : BackgroundService + { + #region Public Constructors + + /// + /// Metodo x calcolo metriche/statistiche di esecuzone realtime + /// + /// + /// + /// + public MetricsCalcService(RouteStatsManager stats, + LuaScriptProvider luaProvider, + IConfiguration config, + IConnectionMultiplexer mux) + { + _stats = stats; + _config = config; + _db = mux.GetDatabase(); + _redisBaseKey = _config.GetValue("ServerConf:RedisBaseKey") ?? "MP_IOC"; + _updateScript = luaProvider.Get("Update"); + } + + #endregion Public Constructors + + #region Protected Methods + + /// + /// Valore SentinelValue x valore minimo in reids/Lua + /// Usiamo un valore molto grande (es. 10 milioni di secondi = ~115 giorni). + /// E' impossibile che una singola richiesta HTTP duri cos� tanto, quindi usarlo come "SentinelValue" � sicuro. + /// + private const string SentinelValue = "999999999"; + + /// + /// Script update Redis in Lua, recuperato dal provider script. + /// Lo script gestisce l'incremento e la logica condizionale per Min/Max in un'unica operazione atomica. + /// + private readonly string _updateScript; + + + // Classe di supporto per l'aggregazione locale dei valori Daily + private class AggregatedStats + { + public long Count; + public double TotalMs; + public double MaxMs; + public double MinMs = double.MaxValue; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var interval = _config.GetValue("RouteMan:MetricCalcIntervalSeconds", 20); + + while (!stoppingToken.IsCancellationRequested) + { + try + { + await Task.Delay(TimeSpan.FromSeconds(interval), stoppingToken); + var snapshot = _stats.Snapshot(); + if (snapshot.Count == 0) continue; + + var adesso = DateTime.Now; + var hourStart = new DateTime(adesso.Year, adesso.Month, adesso.Day, adesso.Hour, 0, 0); + var dayStart = new DateTime(adesso.Year, adesso.Month, adesso.Day, 0, 0, 0); + + if (_db == null) continue; + + var batch = _db.CreateBatch(); + var tasks = new List(); + + // Dizionario per aggregare i valori Daily prima di inviarli a Redis + var dailyAggregates = new Dictionary(); + + foreach (var kv in snapshot) + { + string dest = "IO"; + string method = "NA"; + string machineId = "ALL"; + string rawKey = kv.Key; + if (rawKey.Contains("|")) + { + var splitVal = rawKey.Split("|"); + dest = splitVal[0]; + if (splitVal.Length > 1) + method = splitVal[1]; + if (splitVal.Length > 2) + machineId = splitVal[2]; + } + else + { + method = rawKey; + } + + var stat = kv.Value; + var count = Interlocked.Read(ref stat.Count); + var totalMs = stat.TotalDuration.TotalMilliseconds; + var maxMs = stat.MaxDuration.TotalMilliseconds; + // Se la durata è ancora MaxValue, usiamo la SentinelValue per dire a Redis "non aggiornare" + var minMs = (stat.MinDuration == TimeSpan.MaxValue) + ? double.Parse(SentinelValue) + : stat.MinDuration.TotalMilliseconds; + + // --- LOGICA HOURLY (Per ogni route/method e' una chiave distinta) --- + var hourKey = HourBucketKey(dest, method, hourStart); + var hoursIndex = HoursIndexKey(dest, method); + var hourScore = ToEpochSeconds(hourStart); + + // Usiamo lo script Lua per l'aggiornamento atomico dell'ora + tasks.Add(batch.ScriptEvaluateAsync(_updateScript, + new RedisKey[] { hourKey }, + new RedisValue[] { + count.ToString(CultureInfo.InvariantCulture), + totalMs.ToString(CultureInfo.InvariantCulture), + maxMs.ToString(CultureInfo.InvariantCulture), + minMs.ToString(CultureInfo.InvariantCulture), + SentinelValue + })); + + tasks.Add(batch.SortedSetAddAsync(hoursIndex, hourKey, hourScore)); + + // --- LOGICA DAILY (Aggregazione locale per evitare sovrascritture nel loop) --- + var dayKey = DayBucketKey(dest, machineId, dayStart); + var daysIndex = DaysIndexKey(dest, machineId); + var dayScore = ToEpochSeconds(dayStart); + + if (!dailyAggregates.TryGetValue(dayKey, out var agg)) + { + agg = new AggregatedStats(); + dailyAggregates[dayKey] = agg; + } + + agg.Count += count; + agg.TotalMs += totalMs; + agg.MaxMs = Math.Max(agg.MaxMs, maxMs); + if (minMs != double.MaxValue) + agg.MinMs = Math.Min(agg.MinMs, minMs); + + // Aggiungiamo l'indice al batch + tasks.Add(batch.SortedSetAddAsync(daysIndex, dayKey, dayScore)); + } + + // --- INVIO AGGREGATI DAILY A REDIS --- + foreach (var dayKV in dailyAggregates) + { + var key = dayKV.Key; + var agg = dayKV.Value; + // Se agg.MinMs è inizializzato a double.MaxValue, lo portiamo alla SentinelValue + double finalMin = (agg.MinMs == double.MaxValue || agg.MinMs >= double.Parse(SentinelValue)) + ? double.Parse(SentinelValue) + : agg.MinMs; + + tasks.Add(batch.ScriptEvaluateAsync(_updateScript, + new RedisKey[] { key }, + new RedisValue[] { + agg.Count.ToString(CultureInfo.InvariantCulture), + agg.TotalMs.ToString(CultureInfo.InvariantCulture), + agg.MaxMs.ToString(CultureInfo.InvariantCulture), + finalMin.ToString(CultureInfo.InvariantCulture), + SentinelValue + })); + } + + // Esegui tutto il batch in un unico round-trip + batch.Execute(); + await Task.WhenAll(tasks); + + _stats.Clear(); + } + catch (TaskCanceledException) { } + catch (Exception ex) + { + Log.Error(ex, "Error flushing metrics"); + } + } + } + #endregion Protected Methods + + #region Private Fields + + private static string _redisBaseKey = ""; + private static Logger Log = LogManager.GetCurrentClassLogger(); + private readonly IConfiguration _config; + private readonly IDatabase _db; + private readonly RouteStatsManager _stats; + + #endregion Private Fields + + #region Private Methods + + private static string DayBucketKey(string dest, string machId, DateTime dtRif) + { + return $"{_redisBaseKey}:stats:day:{dest}:{machId}:{dtRif.ToString("yyyyMMdd", CultureInfo.InvariantCulture)}"; + } + + private static string DaysIndexKey(string dest, string machId) + { + return $"{_redisBaseKey}:stats:days:{dest}:{machId}"; + } + + private static string HourBucketKey(string dest, string method, DateTime dtRif) + { + return $"{_redisBaseKey}:stats:hour:{dest}:{method}:{dtRif.ToString("yyyyMMddHH", CultureInfo.InvariantCulture)}"; + } + + private static string HoursIndexKey(string dest, string method) + { + return $"{_redisBaseKey}:stats:hours:{dest}:{method}"; + } + + private static long ToEpochSeconds(DateTime dt) + { + return new DateTimeOffset(dt).ToUnixTimeSeconds(); + } + + #endregion Private Methods + } +} diff --git a/MP-RIOC/Services/MetricsDbFlushService.cs b/MP-RIOC/Services/MetricsDbFlushService.cs new file mode 100644 index 00000000..dbbc9eb4 --- /dev/null +++ b/MP-RIOC/Services/MetricsDbFlushService.cs @@ -0,0 +1,367 @@ +using MP.Data.DbModels.Utils; +using MP.Data.Services.Utils; +using NLog; +using StackExchange.Redis; +using System.Globalization; + +namespace MP.RIOC.Services +{ + public class MetricsDbFlushService : BackgroundService + { + #region Public Constructors + + public MetricsDbFlushService( + IServiceScopeFactory scopeFactory, + IConfiguration config, + IConnectionMultiplexer mux) + { + _scopeFactory = scopeFactory; + _config = config; + _mux = mux; + _db = mux.GetDatabase(); + } + + #endregion Public Constructors + + #region Protected Methods + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var interval = _config.GetValue("RouteMan:MetricFlushIntervalSeconds", 300); + + while (!stoppingToken.IsCancellationRequested) + { + try + { + await ProcessDayLiveMetricsAsync(); + await ProcessHourLiveMetricsAsync(); + await Task.Delay(TimeSpan.FromSeconds(interval), stoppingToken); + } + catch (TaskCanceledException) { break; } + catch (Exception ex) + { + Log.Error(ex, "Error flushing metrics to database"); + } + } + } + + #endregion Protected Methods + + #region Private Fields + + private const double SentinelValue = 999999999; + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); + private readonly IConfiguration _config; + private readonly IDatabase _db; + private readonly IConnectionMultiplexer _mux; + private readonly IServiceScopeFactory _scopeFactory; + + #endregion Private Fields + + #region Private Properties + + private string _redisBaseKey => _config.GetValue("ServerConf:RedisBaseKey") ?? "MP_IOC"; + + #endregion Private Properties + + #region Private Methods + + /// + /// Processing dati giornalieri (da Redis a DB) + /// + /// + private async Task ProcessDayLiveMetricsAsync() + { + var aggrRecordsToInsert = new List(); + var keysToDelete = new List(); + + bool deleteConfirmed = _config.GetValue("RouteMan:DeleteExpiredMetrics", false); + DateTime now = DateTime.Now; + + // Confini temporali per proteggere i dati in corso + DateTime currentDayStart = new DateTime(now.Year, now.Month, now.Day, 0, 0, 0); + + var endpoints = _mux.GetEndPoints(); + + foreach (var endpoint in endpoints) + { + var server = _mux.GetServer(endpoint); + + if (server.IsReplica) + { + continue; + } + + string[] patternsToScan = { + $"{_redisBaseKey}:stats:days:*" + }; + + foreach (var pattern in patternsToScan) + { + // Nota: KeyScanAsync/KeysAsync e' disponibile su IServer + await foreach (var indexKey in server.KeysAsync(pattern: pattern)) + { + if (string.IsNullOrEmpty($"{indexKey}")) continue; + + // CORREZIONE: Utilizzo di SortedSetRangeByRankAsync con range 0 a -1 per prendere tutto il set + var memberKeys = await _db.SortedSetRangeByRankAsync(indexKey, 0, -1); + + foreach (var statKey in memberKeys) + { + var sKey = (RedisKey)$"{statKey}"; + if (!TryParseKeyMetadata(sKey, out string dest, out string method, out string machId, out DateTime timestamp, out bool isHourType)) + continue; + + // Verifica se la chiave fosse scaduta rispetto all'orario corrente + bool isExpired = isHourType + //? timestamp < currentHourStart + ? false + : timestamp < currentDayStart; + + // Se fosse scaduta e abbiamo il permesso, segnamola per la cancellazione + if (isExpired && deleteConfirmed) + { + keysToDelete.Add(sKey); + } + + // Recupero dati dalla Hash + var hashData = await _db.HashGetAllAsync(sKey); + if (hashData.Length == 0) continue; + + var dict = hashData.ToDictionary(x => x.Name.ToString(), x => x.Value.ToString()); + + if (dict.TryGetValue("count", out var countStr) && + dict.TryGetValue("totalMs", out var totalMsStr)) + { + long count = long.Parse(countStr); + count = long.Parse(countStr); + double totalMs = double.Parse(totalMsStr, CultureInfo.InvariantCulture); + + double maxMs = dict.ContainsKey("maxMs") ? double.Parse(dict["maxMs"], CultureInfo.InvariantCulture) : 0; + double minMs = dict.ContainsKey("minMs") ? double.Parse(dict["minMs"], CultureInfo.InvariantCulture) : 0; + + // TUA CORREZIONE: Reset sentinella per evitare valori fuori scala nel DB + if (minMs >= SentinelValue) minMs = SentinelValue; + if (maxMs >= SentinelValue) maxMs = SentinelValue; + + if (count <= 0) continue; + + aggrRecordsToInsert.Add(new StatsAggregatedModel + { + Destination = dest, + MachineId = machId, + Hour = timestamp, + RequestCount = count, + AvgDuration = totalMs / count, + MinDuration = minMs, + MaxDuration = maxMs, + NoReply = 0 + }); + } + } + } + } + } + + // --- FASE UPSERT DB --- + if (aggrRecordsToInsert.Count > 0) + { + await using var scope = _scopeFactory.CreateAsyncScope(); + var aggrService = scope.ServiceProvider.GetRequiredService(); + await aggrService.UpsertManyAsync(aggrRecordsToInsert, false); + Log.Info($"[DAY] Upserted {aggrRecordsToInsert.Count} records to DB"); + } + + // --- FASE PULIZIA REDIS --- + if (deleteConfirmed && keysToDelete.Count > 0) + { + var batch = _db.CreateBatch(); + int deletedCount = 0; + foreach (var key in keysToDelete) + { + _ = batch.KeyDeleteAsync(key); + deletedCount++; + } + batch.Execute(); + Log.Info($"[CLEANUP DAY] Deleted {deletedCount} expired metric keys from Redis"); + } + } + + /// + /// Processing dati orari (da Redis a DB) + /// + /// + private async Task ProcessHourLiveMetricsAsync() + { + var detailRecordsToInsert = new List(); + var keysToDelete = new List(); + + bool deleteConfirmed = _config.GetValue("RouteMan:DeleteExpiredMetrics", false); + DateTime now = DateTime.Now; + + // Confini temporali per proteggere i dati in corso + DateTime currentHourStart = new DateTime(now.Year, now.Month, now.Day, now.Hour, 0, 0); + DateTime currentDayStart = new DateTime(now.Year, now.Month, now.Day, 0, 0, 0); + + var endpoints = _mux.GetEndPoints(); + + foreach (var endpoint in endpoints) + { + var server = _mux.GetServer(endpoint); + + if (server.IsReplica) + { + continue; + } + + string[] patternsToScan = { + $"{_redisBaseKey}:stats:hours:*" + }; + + foreach (var pattern in patternsToScan) + { + // Nota: KeyScanAsync/KeysAsync e' disponibile su IServer + await foreach (var indexKey in server.KeysAsync(pattern: pattern)) + { + if (string.IsNullOrEmpty($"{indexKey}")) continue; + + // CORREZIONE: Utilizzo di SortedSetRangeByRankAsync con range 0 a -1 per prendere tutto il set + var memberKeys = await _db.SortedSetRangeByRankAsync(indexKey, 0, -1); + + foreach (var statKey in memberKeys) + { + var sKey = (RedisKey)$"{statKey}"; + if (!TryParseKeyMetadata(sKey, out string dest, out string method, out string machId, out DateTime timestamp, out bool isHourType)) + continue; + + // Verifica se la chiave fosse scaduta rispetto all'orario corrente + bool isExpired = isHourType + ? timestamp < currentHourStart + : false; + //: timestamp < currentDayStart; + + // Se fosse scaduta e abbiamo il permesso, segnamola per la cancellazione + if (isExpired && deleteConfirmed) + { + keysToDelete.Add(sKey); + } + + // Recupero dati dalla Hash + var hashData = await _db.HashGetAllAsync(sKey); + if (hashData.Length == 0) continue; + + var dict = hashData.ToDictionary(x => x.Name.ToString(), x => x.Value.ToString()); + + if (dict.TryGetValue("count", out var countStr) && + dict.TryGetValue("totalMs", out var totalMsStr)) + { + long count = long.Parse(countStr); + count = long.Parse(countStr); + double totalMs = double.Parse(totalMsStr, CultureInfo.InvariantCulture); + + double maxMs = dict.ContainsKey("maxMs") ? double.Parse(dict["maxMs"], CultureInfo.InvariantCulture) : 0; + double minMs = dict.ContainsKey("minMs") ? double.Parse(dict["minMs"], CultureInfo.InvariantCulture) : 0; + + // TUA CORREZIONE: Reset sentinella per evitare valori fuori scala nel DB + if (minMs >= SentinelValue) minMs = SentinelValue; + if (maxMs >= SentinelValue) maxMs = SentinelValue; + + if (count <= 0) continue; + + detailRecordsToInsert.Add(new StatsDetailModel + { + Destination = dest, + Type = method, + Hour = timestamp, + RequestCount = count, + AvgDuration = totalMs / count, + MinDuration = minMs, + MaxDuration = maxMs, + NoReply = 0 + }); + } + } + } + } + } + + // --- FASE UPSERT DB --- + if (detailRecordsToInsert.Count > 0) + { + await using var scope = _scopeFactory.CreateAsyncScope(); + var detailService = scope.ServiceProvider.GetRequiredService(); + await detailService.UpsertManyAsync(detailRecordsToInsert, false); + Log.Info($"[HOUR] Upserted {detailRecordsToInsert.Count} records to DB"); + } + + // --- FASE PULIZIA REDIS --- + if (deleteConfirmed && keysToDelete.Count > 0) + { + var batch = _db.CreateBatch(); + int deletedCount = 0; + foreach (var key in keysToDelete) + { + _ = batch.KeyDeleteAsync(key); + deletedCount++; + } + batch.Execute(); + Log.Info($"[CLEANUP HOUR] Deleted {deletedCount} expired metric keys from Redis"); + } + } + + private bool TryParseKeyMetadata(RedisKey key, out string dest, out string method, out string machId, out DateTime timestamp, out bool isHourType) + { + dest = "NA"; + method = "NA"; + machId = "ALL"; + timestamp = DateTime.MinValue; + isHourType = true; + try + { + string k = key.ToString(); + string relativeKey = k.Replace($"{_redisBaseKey}:", ""); + var parts = relativeKey.Split(':'); + + if (parts.Length < 4) return false; + + string type = parts[1]; // "hour" o "day" + dest = parts[2]; + + if (type == "hour") + { + isHourType = true; + method = parts[3]; + if (parts.Length >= 5 && DateTime.TryParseExact(parts[4], "yyyyMMddHH", CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt)) + { + timestamp = dt; + return true; + } + } + else if (type == "day") + { + isHourType = false; + method = "DAILY"; + string rawDate = ""; + if (parts.Length >= 5) + { + machId = parts[3]; + rawDate = parts[4]; + } + else + { + rawDate = parts[3]; + } + if (DateTime.TryParseExact(rawDate, "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt)) + { + timestamp = dt; + return true; + } + } + } + catch { } + return false; + } + + #endregion Private Methods + } +} diff --git a/MP-RIOC/appsettings.json b/MP-RIOC/appsettings.json index 4583eef3..0f17cadb 100644 --- a/MP-RIOC/appsettings.json +++ b/MP-RIOC/appsettings.json @@ -72,7 +72,6 @@ "useFactory": true }, "ConnectionStrings": { - "MP.Utils": "Server=SQL2016DEV;Database=MoonPro_Utils; User ID=sa;Password=keyhammer16; integrated security=False; App=MP.IOC;", "Redis": "redis.ufficio:26379,serviceName=devel,DefaultDatabase=5,connectTimeout=5000,syncTimeout=5000,asyncTimeout=5000,abortConnect=false,ssl=false", "RedisAdmin": "redis.ufficio:26379,serviceName=devel,DefaultDatabase=5,connectTimeout=5000,syncTimeout=5000,asyncTimeout=5000,abortConnect=false,ssl=false,allowAdmin=true" } diff --git a/MP-RIOC/dotnet-tools.json b/MP-RIOC/dotnet-tools.json new file mode 100644 index 00000000..ce0aab73 --- /dev/null +++ b/MP-RIOC/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "10.0.7", + "commands": [ + "dotnet-ef" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/MP.IOC/MP.IOC.csproj b/MP.IOC/MP.IOC.csproj index d1c88af3..cc421694 100644 --- a/MP.IOC/MP.IOC.csproj +++ b/MP.IOC/MP.IOC.csproj @@ -4,7 +4,7 @@ net8.0 enable enable - 8.16.2605.611 + 8.16.2605.808 diff --git a/MP.IOC/Resources/ChangeLog.html b/MP.IOC/Resources/ChangeLog.html index f22519c1..c2c936a0 100644 --- a/MP.IOC/Resources/ChangeLog.html +++ b/MP.IOC/Resources/ChangeLog.html @@ -1,6 +1,6 @@ Modulo MP-IOC -

Versione: 8.16.2605.611

+

Versione: 8.16.2605.808


Note di rilascio:
  • diff --git a/MP.IOC/Resources/VersNum.txt b/MP.IOC/Resources/VersNum.txt index a09c6432..a37f9c6b 100644 --- a/MP.IOC/Resources/VersNum.txt +++ b/MP.IOC/Resources/VersNum.txt @@ -1 +1 @@ -8.16.2605.611 +8.16.2605.808 diff --git a/MP.IOC/Resources/manifest.xml b/MP.IOC/Resources/manifest.xml index 08c9065b..2f8a10b2 100644 --- a/MP.IOC/Resources/manifest.xml +++ b/MP.IOC/Resources/manifest.xml @@ -1,6 +1,6 @@ - 8.16.2605.611 + 8.16.2605.808 https://nexus.steamware.net/repository/SWS/MP-IOC/stable/LAST/MP.IOC.zip https://nexus.steamware.net/repository/SWS/MP-IOC/stable/LAST/ChangeLog.html false