Aggiunta progetto API Routing semplificata (solo routing)

This commit is contained in:
Samuele Locatelli
2026-05-08 08:03:23 +02:00
parent 0f54b832e7
commit f95e7c441b
23 changed files with 6004 additions and 3 deletions
+2 -2
View File
@@ -28,9 +28,9 @@
<PackageVersion Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="6.0.17" />
<PackageVersion Include="MongoDB.Driver" Version="2.19.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="NLog" Version="6.1.2" />
<PackageVersion Include="NLog" Version="6.1.3" />
<PackageVersion Include="NLog.Targets.OpenTelemetryProtocol" Version="1.2.6" />
<PackageVersion Include="NLog.Web.AspNetCore" Version="6.1.2" />
<PackageVersion Include="NLog.Web.AspNetCore" Version="6.1.3" />
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.15.3" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.3" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.15.3" />
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
@MP_RIOC_HostAddress = http://localhost:5290
GET {{MP_RIOC_HostAddress}}/weatherforecast/
Accept: application/json
###
+4
View File
@@ -0,0 +1,4 @@
<Solution>
<Project Path="../MP.Core/MP.Core.csproj" />
<Project Path="MP.RIOC.csproj" />
</Solution>
+41
View File
@@ -0,0 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>MP_RIOC</RootNamespace>
<Version>8.16.2605.808</Version>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Data\MpDataService.cs" />
</ItemGroup>
<ItemGroup>
<Content Remove="compilerconfig.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="NLog" />
<PackageReference Include="NLog.Web.AspNetCore" />
<PackageReference Include="Swashbuckle.AspNetCore" />
<PackageReference Include="Yarp.ReverseProxy" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MP.Core\MP.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Data\" />
</ItemGroup>
<ItemGroup>
<None Include="compilerconfig.json" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="powershell.exe -ExecutionPolicy Unrestricted -NoProfile -NonInteractive -File $(ProjectDir)\post-build.ps1 -ProjectDir $(ProjectDir) -ProjectPath $(ProjectPath)" />
</Target>
</Project>
+25
View File
@@ -0,0 +1,25 @@
@page
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<title>MP.RIOC - Router Status</title>
<style>
body { font-family: sans-serif; background: #f4f4f4; padding: 50px; }
.card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
.status-on { color: green; font-weight: bold; }
</style>
</head>
<body>
<div class="card">
<h1>MP.RIOC</h1>
<b>MAPO Server Router</b>
<p>Stato del Servizio: <span class="status-on">ONLINE</span></p>
<hr />
<p>Questo server smista il traffico tra MP.IO (API Legacy dotNet 4.7.2) e MP.IOC (nuove API .NET 8)</p>
<small>Versione Router: @System.Reflection.Assembly.GetExecutingAssembly().GetName().Version</small>
</div>
</body>
</html>
+12
View File
@@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace MP.RIOC.Pages
{
public class IndexModel : PageModel
{
public void OnGet()
{
}
}
}
+131
View File
@@ -0,0 +1,131 @@
using MP.RIOC.Services;
using NLog;
using NLog.Web;
using StackExchange.Redis;
using System.Diagnostics;
using System.Net;
using System.Reflection;
var builder = WebApplication.CreateBuilder(args);
// recupero env corrente
var env = builder.Environment;
var logger = LogManager.Setup()
.LoadConfigurationFromAppSettings()
.GetCurrentClassLogger();
var assemblyVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString();
logger.Info($"MP.RIOC | Program.cs: startup | v.{assemblyVersion}");
logger.Info($"Current ASPNETCORE_ENVIRONMENT: {env.EnvironmentName}");
// Config setup
ConfigurationManager configuration = builder.Configuration;
// REDIS setup
logger.Info("Config OK");
string confRedis = configuration.GetConnectionString("Redis");
string redisSrvAddr = confRedis.Substring(0, confRedis.IndexOf(":"));
logger.Info("Setup REDIS OK");
// 1. Configurazione dell'invoker personalizzato (Risolve i tuoi errori)
var httpClientInvoker = new HttpMessageInvoker(new SocketsHttpHandler
{
UseProxy = false,
AllowAutoRedirect = false,
AutomaticDecompression = DecompressionMethods.None,
UseCookies = false,
// Correzione per il tracing: usa il propagatore corrente di sistema
ActivityHeadersPropagator = DistributedContextPropagator.Current,
ConnectTimeout = TimeSpan.FromSeconds(15),
// Gestione certificato (ignora errori per localhost/test)
SslOptions = new System.Net.Security.SslClientAuthenticationOptions
{
RemoteCertificateValidationCallback = (sender, cert, chain, sslPolicyErrors) => true
}
});
builder.Services.AddSingleton(httpClientInvoker);
builder.Services.AddHttpForwarder();
// avvio oggetto shared x redis...
var redisMux = ConnectionMultiplexer.Connect(confRedis);
builder.Services.AddSingleton<IConnectionMultiplexer>(redisMux);
// Registrazione dei servizi custom
builder.Services.AddSingleton<PreserveBodyTransformer>();
builder.Services.AddSingleton<RouteStatsManager>();
logger.Info("Standard service configured");
// WeightProvider: Redis/Memory da config
var weightOnRedis = builder.Configuration.GetValue<bool>("ServerConf:RedisWeight", false);
if (weightOnRedis)
{
builder.Services.AddSingleton<IWeightProvider, RedisWeightProvider>();
}
else
{
builder.Services.AddSingleton<IWeightProvider, InMemoryWeightProvider>();
}
logger.Info($"Weight service configured | use Redis: {weightOnRedis}");
// RouteManager registration (singleton)
builder.Services.AddSingleton<RouteManager>();
logger.Info("Singleton Route Manager registered");
// aggiunta pagina razor di stato
builder.Services.AddRazorPages();
var app = builder.Build();
// 1. Configurazione Base Path
string baseUrl = configuration.GetValue<string>("ServerConf:BaseUrlIoc") ?? "/MP/RIOC";
app.UsePathBase(baseUrl);
// 2. Middleware statici (essenziali per CSS/JS delle pagine Razor)
app.UseStaticFiles();
// 3. Abilita il Routing (necessario per MapGet e MapRazorPages)
app.UseRouting();
// 4. Logging middleware
app.Use(async (ctx, next) =>
{
logger.Debug($"Incoming request PathBase='{ctx.Request.PathBase}' Path='{ctx.Request.Path}'");
await next();
});
// 5. Il cuore del Proxy (MapWhen è terminale per le richieste che lo soddisfano)
string routePath = configuration.GetValue<string>("ServerConf:RoutePath") ?? "/api/IOB";
string fullPath = $"{baseUrl}{routePath}".Replace("//", "/");
logger.Info($"BaseUrl: {baseUrl}");
app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments(routePath, StringComparison.OrdinalIgnoreCase),
builder =>
{
builder.Run(async ctx =>
{
var routeManager = ctx.RequestServices.GetRequiredService<RouteManager>();
await routeManager.HandleAsync(ctx);
});
});
// 6. Definizione degli Endpoints locali
app.MapRazorPages();
app.MapGet("/router-status", () => Results.Ok(new
{
Status = "Online",
Version = assemblyVersion,
Mode = weightOnRedis ? "Redis" : "InMemory",
Time = DateTime.Now
}));
// 7. Fallback "intelligente"
// Invece di app.Run, usiamo MapFallback che viene eseguito SOLO se nessun altro endpoint o MapWhen ha risposto
app.MapFallback(async context =>
{
context.Response.StatusCode = 404;
await context.Response.WriteAsync("Router: Endpoint non trovato o non mappato.");
});
app.Run();
+41
View File
@@ -0,0 +1,41 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:15280",
"sslPort": 44323
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "MP/RIOC/api/IOB/",
"applicationUrl": "http://localhost:5290",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "MP/RIOC/api/IOB/",
"applicationUrl": "https://localhost:7120;http://localhost:5290",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "MP/RIOC/api/IOB/",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
+29
View File
@@ -0,0 +1,29 @@
using MP.Core.DTO;
namespace MP.RIOC.Services
{
public interface IWeightProvider
{
#region Public Methods
/// <summary>
/// Ritorna l'intero elenco dei weight attivi nel formato WeightDTO
/// </summary>
/// <returns></returns>
Task<List<WeightDTO>> GetAllWeightsAsync();
/// <summary>
/// Ritorna la coppia (oldWeight, newWeight) per scegliere dove instradare il metodo tra i 2 sistemi API.
/// </summary>
(int oldWeight, int newWeight) GetWeightsFor(string method);
/// <summary>
/// Aggiorna/Aggiuinge il valore del weight richiesto
/// </summary>
/// <param name=""></param>
/// <returns></returns>
bool UpsertWeight(WeightDTO updRecord);
#endregion Public Methods
}
}
@@ -0,0 +1,56 @@
using MP.Core.DTO;
using System.Collections.Concurrent;
namespace MP.RIOC.Services
{
public class InMemoryWeightProvider : IWeightProvider
{
private readonly ConcurrentDictionary<string, (int oldW, int newW)> _map = new();
private readonly int _defaultOld;
private readonly int _defaultNew;
public InMemoryWeightProvider(IConfiguration config)
{
_defaultOld = config.GetValue<int>("RouteMan:DefaultWeightOld", 100);
_defaultNew = config.GetValue<int>("RouteMan:DefaultWeightNew", 0);
}
public (int oldWeight, int newWeight) GetWeightsFor(string method)
{
if (string.IsNullOrEmpty(method)) method = "unknown";
return _map.GetOrAdd(method, _ => (_defaultOld, _defaultNew));
}
public void SetWeights(string method, int oldWeight, int newWeight)
{
_map[method] = (Math.Clamp(oldWeight, 0, 100), Math.Clamp(newWeight, 0, 100));
}
public async Task<List<WeightDTO>> GetAllWeightsAsync()
{
var result = new List<WeightDTO>();
await Task.Delay(1);
foreach (var kvp in _map)
{
result.Add(new WeightDTO
{
Method = kvp.Key,
OldWeight = Math.Clamp(kvp.Value.oldW, 0, 100),
NewWeight = Math.Clamp(kvp.Value.newW, 0, 100)
});
}
return result;
}
public bool UpsertWeight(WeightDTO updRecord)
{
if (updRecord == null || string.IsNullOrEmpty(updRecord.Method))
return false;
_map[updRecord.Method] = (Math.Clamp(updRecord.OldWeight, 0, 100), Math.Clamp(updRecord.NewWeight, 0, 100));
return true;
}
}
}
@@ -0,0 +1,38 @@
using Yarp.ReverseProxy.Forwarder;
namespace MP.RIOC.Services
{
public class PreserveBodyTransformer : HttpTransformer
{
public override async ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix, CancellationToken cancellationToken)
{
// Chiama il base (usa overload con cancellationToken)
await base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix, cancellationToken);
// Imposta il method correttamente
proxyRequest.Method = new HttpMethod(httpContext.Request.Method);
// Se vuoi leggere/loggare il body, abilita buffering e rewind.
// NON assegnare proxyRequest.Content: YARP copierà il body da HttpContext.Request.
if (httpContext.Request.ContentLength > 0 || httpContext.Request.Body.CanRead)
{
// Abilita buffering solo se necessario (attenzione a payload grandi)
httpContext.Request.EnableBuffering();
// Rewind per sicurezza
httpContext.Request.Body.Position = 0;
// Se vuoi leggere il body per logging, fallo qui ma non sostituire proxyRequest.Content.
// Esempio (opzionale): leggere senza consumare
// using var sr = new StreamReader(httpContext.Request.Body, leaveOpen: true);
// var bodyText = await sr.ReadToEndAsync();
// httpContext.Request.Body.Position = 0;
}
}
public override ValueTask<bool> TransformResponseAsync(HttpContext httpContext, HttpResponseMessage? proxyResponse, CancellationToken cancellationToken)
{
return base.TransformResponseAsync(httpContext, proxyResponse, cancellationToken);
}
}
}
+157
View File
@@ -0,0 +1,157 @@
using MP.Core.DTO;
using StackExchange.Redis;
namespace MP.RIOC.Services
{
public class RedisWeightProvider : IWeightProvider
{
#region Public Constructors
public RedisWeightProvider(IConnectionMultiplexer mux, IConfiguration config)
{
_config = config;
_db = mux.GetDatabase();
_mux = mux;
_defaultOld = config.GetValue<int>("RouteMan:DefaultWeightOld", 100);
_defaultNew = config.GetValue<int>("RouteMan:DefaultWeightNew", 0);
_redisBaseKey = config.GetValue<string>("ServerConf:RedisBaseKey") ?? "MP_IOC";
_keyPrefix = $"{_redisBaseKey}:route_weight:";
}
#endregion Public Constructors
#region Public Methods
/// <summary>
/// Ritorna (oldWeight, newWeight) per il metodo. Se non esiste, crea la chiave con i default.
/// </summary>
public (int oldWeight, int newWeight) GetWeightsFor(string method)
{
if (string.IsNullOrEmpty(method)) method = "unknown";
var key = _keyPrefix + method;
// Leggi entrambi i campi
var oldVal = _db.HashGet(key, "old");
var newVal = _db.HashGet(key, "new");
// Se entrambi mancanti, inizializza con default (usando HSet con When.NotExists per evitare overwrite)
if (oldVal.IsNull && newVal.IsNull)
{
// Imposta i campi singolarmente con When.NotExists per evitare overwrite
_db.HashSet(key, "old", _defaultOld, When.NotExists);
_db.HashSet(key, "new", _defaultNew, When.NotExists);
// Rileggi per essere sicuri
oldVal = _db.HashGet(key, "old");
newVal = _db.HashGet(key, "new");
}
// Se uno dei due manca, impostalo al default (non sovrascrive l'altro)
if (oldVal.IsNull)
{
_db.HashSet(key, "old", _defaultOld, When.NotExists);
oldVal = _defaultOld;
}
if (newVal.IsNull)
{
_db.HashSet(key, "new", _defaultNew, When.NotExists);
newVal = _defaultNew;
}
if (!int.TryParse(oldVal.ToString(), out var oldW)) oldW = _defaultOld;
if (!int.TryParse(newVal.ToString(), out var newW)) newW = _defaultNew;
// clamp 0..100
oldW = Math.Clamp(oldW, 0, 100);
newW = Math.Clamp(newW, 0, 100);
return (oldW, newW);
}
// API per aggiornare i pesi a runtime (opzionale)
public void SetWeights(string method, int oldWeight, int newWeight)
{
var key = _keyPrefix + (string.IsNullOrEmpty(method) ? "unknown" : method);
_db.HashSet(key, new HashEntry[] {
new HashEntry("old", Math.Clamp(oldWeight,0,100)),
new HashEntry("new", Math.Clamp(newWeight,0,100))
});
}
public async Task<List<WeightDTO>> GetAllWeightsAsync()
{
var result = new List<WeightDTO>();
var server = _mux.GetServer(_mux.GetEndPoints().First());
if (server.IsReplica)
{
return result;
}
await foreach (var key in server.KeysAsync(pattern: $"{_keyPrefix}*"))
{
var methodName = KeyToString(key.ToString());
if (string.IsNullOrEmpty(methodName)) continue;
var oldVal = _db.HashGet(key, "old");
var newVal = _db.HashGet(key, "new");
int oldW = 100;
int newW = 0;
if (!oldVal.IsNull && int.TryParse(oldVal.ToString(), out var parsedOld))
oldW = Math.Clamp(parsedOld, 0, 100);
if (!newVal.IsNull && int.TryParse(newVal.ToString(), out var parsedNew))
newW = Math.Clamp(parsedNew, 0, 100);
result.Add(new WeightDTO { Method = methodName, OldWeight = oldW, NewWeight = newW });
}
// riordino desc x NEW poi alfabetico...
result = result
.OrderByDescending(x => x.NewWeight)
.ThenBy(x => x.Method)
.ToList();
return result;
}
public bool UpsertWeight(WeightDTO updRecord)
{
if (updRecord == null || string.IsNullOrEmpty(updRecord.Method))
return false;
var key = _keyPrefix + updRecord.Method;
_db.HashSet(key, new HashEntry[] {
new HashEntry("old", Math.Clamp(updRecord.OldWeight, 0, 100)),
new HashEntry("new", Math.Clamp(updRecord.NewWeight, 0, 100))
});
return true;
}
private string KeyToString(string key)
{
if (string.IsNullOrEmpty(key)) return "";
var prefix = _keyPrefix ?? "";
if (key.StartsWith(prefix))
return key.Substring(prefix.Length);
return key;
}
#endregion Public Methods
#region Private Fields
private static string _keyPrefix = "route_weight:";
private static string _redisBaseKey = "";
private readonly IConfiguration _config;
private readonly IDatabase _db;
private readonly IConnectionMultiplexer _mux;
private readonly int _defaultNew;
private readonly int _defaultOld;
#endregion Private Fields
}
}
+167
View File
@@ -0,0 +1,167 @@
using NLog;
using System.Diagnostics;
using Yarp.ReverseProxy.Forwarder;
namespace MP.RIOC.Services
{
public class RouteManager
{
private readonly IHttpForwarder _forwarder;
private readonly HttpMessageInvoker _httpClientInvoker;
private readonly PreserveBodyTransformer _transformer;
private readonly RouteStatsManager _stats;
private readonly IWeightProvider _weightProvider;
private readonly IConfiguration _config;
private readonly ForwarderRequestConfig _forwarderConfig;
public RouteManager(
IHttpForwarder forwarder,
HttpMessageInvoker httpClientInvoker,
PreserveBodyTransformer transformer,
RouteStatsManager stats,
IWeightProvider weightProvider,
IConfiguration config)
{
_forwarder = forwarder;
_httpClientInvoker = httpClientInvoker;
_transformer = transformer;
_stats = stats;
_weightProvider = weightProvider;
_config = config;
_routePath = _config.GetValue<string>("ServerConf:RoutePath") ?? "/api/IOB";
_forwarderConfig = new ForwarderRequestConfig
{
ActivityTimeout = TimeSpan.FromSeconds(30),
// Parse della versione (es. "1.1")
Version = Version.Parse(_config.GetValue<string>("ServerConf:HttpVersion") ?? "1.1"),
// Policy per la versione
VersionPolicy = _config.GetValue<HttpVersionPolicy?>("ServerConf:HttpVersionPolicy")
?? HttpVersionPolicy.RequestVersionExact
};
}
private string _routePath = "";
private static Logger Log = LogManager.GetCurrentClassLogger();
public async Task HandleAsync(HttpContext context)
{
var sw = Stopwatch.StartNew();
var routePrefix = new PathString(_routePath);
var fullPrefix = context.Request.PathBase.Add(routePrefix);
string relativePath;
string query = context.Request.QueryString.HasValue ? context.Request.QueryString.Value : "";
if (context.Request.Path.StartsWithSegments(fullPrefix, out var remaining))
{
relativePath = remaining.Value.TrimStart('/');
}
else if (context.Request.Path.StartsWithSegments(routePrefix, out remaining))
{
relativePath = remaining.Value.TrimStart('/');
}
else
{
var fullPath = (context.Request.PathBase + context.Request.Path).Value ?? "";
var idx = fullPath.IndexOf("/RIOB/", StringComparison.OrdinalIgnoreCase);
relativePath = idx >= 0 ? fullPath[(idx + "/RIOB/".Length)..] : fullPath.TrimStart('/');
}
Log.Debug($"PathBase={context.Request.PathBase} | Path={context.Request.Path} | relativePath={relativePath}");
// Procedo a calcolare metodo e ID...
string metodo = "/";
string id = "ALL";
if (!string.IsNullOrEmpty(relativePath))
{
// Rimuovo eventuale query string
var pathOnly = relativePath.Split('?')[0];
// Splitto per /
var parts = pathOnly.Split('/', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length > 0) metodo = parts[0];
if (parts.Length > 1) id = parts[1];
}
Log.Debug($"Metodo: {metodo} | machineId: {id}");
var (oldW, newW) = _weightProvider.GetWeightsFor(metodo);
var pickNew = DecideByWeights(oldW, newW);
var target = pickNew ? "IOC" : "IO";
// Costruisci destination base in base al target
var destBase = pickNew
? _config["ServerConf:NewApiUrl"]
: _config["ServerConf:OldApiUrl"];
if (string.IsNullOrEmpty(destBase))
{
context.Response.StatusCode = 502;
await context.Response.WriteAsync("Destination not configured");
return;
}
if (!destBase.EndsWith("/")) destBase += "/";
// salva path originale per ripristino
var originalPath = context.Request.Path;
var originalPathBase = context.Request.PathBase;
try
{
string sKey = $"{target}|{metodo}|{id}";
// Registra scelta
_stats.Record(sKey);
// imposta la Path che vogliamo che YARP appenda al destBase
context.Request.Path = new PathString("/" + relativePath);
// opzionale: se vuoi che PathBase sia vuoto per il backend, impostalo così
context.Request.PathBase = PathString.Empty;
Log.Debug($"Forwarding to base {destBase} with forwarded path {context.Request.Path} | original PathBase:{originalPathBase} | Path={originalPath})");
var error = await _forwarder.SendAsync(context, destBase, _httpClientInvoker, _forwarderConfig, _transformer, context.RequestAborted);
sw.Stop();
_stats.RecordDuration(sKey, sw.Elapsed);
if (error != ForwarderError.None)
{
var feat = context.GetForwarderErrorFeature();
Log.Error(feat?.Exception, "Forwarder error to {DestBase}", destBase);
if (!context.Response.HasStarted)
{
context.Response.StatusCode = 502;
await context.Response.WriteAsync($"Forward error: {feat?.Exception?.Message}");
}
}
}
finally
{
// ripristina i path originali per non rompere la pipeline successiva
context.Request.Path = originalPath;
context.Request.PathBase = originalPathBase;
}
}
private bool DecideByWeights(int oldW, int newW)
{
bool result = false;
// se entrambi zero -> prefer legacy
var total = oldW + newW;
if (total <= 0)
{
result = false;
}
else
{
var rnd = Random.Shared.NextDouble(); // 0..1
result = rnd < (double)newW / total;
}
return result;
}
}
}
+71
View File
@@ -0,0 +1,71 @@
using System.Collections.Concurrent;
namespace MP.RIOC.Services
{
public class RouteStats
{
public long Count;
public TimeSpan TotalDuration = TimeSpan.Zero;
public TimeSpan MaxDuration = TimeSpan.Zero;
public TimeSpan MinDuration = TimeSpan.MaxValue;
public ConcurrentDictionary<int, long> StatusCodes = new();
}
public class RouteStatsManager
{
private readonly ConcurrentDictionary<string, RouteStats> _map = new();
/// <summary>
/// Registrazione del metodo + destinazione
/// </summary>
/// <param name="dest_method"></param>
public void Record(string dest_method)
{
var stat = _map.GetOrAdd(dest_method, _ => new RouteStats());
Interlocked.Increment(ref stat.Count);
}
/// <summary>
/// Registrazione destinazione+metodo
/// </summary>
/// <param name="dest_method">chiave dest+metodo x salvataggio statistiche</param>
/// <param name="duration"></param>
public void RecordDuration(string dest_method, TimeSpan duration)
{
if (_map.TryGetValue(dest_method, out var stat))
{
lock (stat)
{
stat.TotalDuration += duration;
if (stat.MaxDuration < duration)
{
stat.MaxDuration = duration;
}
if (stat.MinDuration > duration)
{
stat.MinDuration = duration;
}
}
}
}
public void RecordStatusCode(string method, int statusCode)
{
if (_map.TryGetValue(method, out var stat))
{
stat.StatusCodes.AddOrUpdate(statusCode, 1, (_, v) => v + 1);
}
}
public IReadOnlyDictionary<string, RouteStats> Snapshot()
{
return _map.ToDictionary(kv => kv.Key, kv => kv.Value);
}
public void Clear()
{
_map.Clear();
}
}
}
+13
View File
@@ -0,0 +1,13 @@
namespace MP.RIOC
{
public class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string? Summary { get; set; }
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
+79
View File
@@ -0,0 +1,79 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Yarp": "Debug",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"NLog": {
"variables": {
"baseFileDir": "${basedir}/logs/",
"layout": "${longdate} | ${uppercase:${level}} | ${logger:shortName=false} | ${message}"
},
"extensions": [
{ "assembly": "NLog.Extensions.Logging" },
{ "assembly": "NLog.Web.AspNetCore" }
],
"throwConfigExceptions": true,
"targets": {
"async": true,
"logfile": {
"type": "File",
"fileName": "${basedir}/logs/${shortdate}.log",
"archiveEvery": "Day",
"archiveFileName": "${basedir}/logs/old/${shortdate}_{#}.log",
"archiveNumbering": "DateAndSequence",
"archiveAboveSize": "10240000",
"archiveDateFormat": "HH",
"maxArchiveFiles": "60",
"maxArchiveDays": "30"
},
"logconsole": {
"type": "ColoredConsole",
"layout": "${longdate} | ${uppercase:${level}} | ${logger:shortName=true} | ${message}"
}
},
"rules": [
{
"logger": "*",
"minLevel": "Trace",
"writeTo": "logconsole"
},
{
"logger": "*",
"minLevel": "Info",
"writeTo": "logfile"
}
]
},
"CodApp": "MP.RIOC",
"RouteMan": {
"MetricCalcIntervalSeconds": 10,
"MetricFlushIntervalSeconds": 60,
"DefaultWeightOld": 100,
"DefaultWeightNew": 0,
"DeleteExpiredMetrics": true
},
"ServerConf": {
"RoutePath": "/api/IOB",
"HttpVersion": "1.1",
"HttpVersionPolicy": "RequestVersionExact",
"OldApiUrl": "https://iis01.egalware.com/MP/IO/IOB/",
"NewApiUrl": "https://iis01.egalware.com/MP/IOC/api/IOB/",
"BaseUrlIoc": "/MP/RIOC/",
"MpIoNS": "MoonPro:SQL2016DEV:MoonPro",
"RedisBaseKey": "MP-IOC",
"RedisWeight": true,
"SafePages": "Index",
"redisLongTimeCache": 60,
"redisShortTimeCache": 30,
"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"
}
}
+6
View File
@@ -0,0 +1,6 @@
[
{
"outputFile": "Components/Layout/MainLayout.razor.css",
"inputFile": "Components/Layout/MainLayout.razor.less"
}
]
+59
View File
@@ -0,0 +1,59 @@
{
"compilers": {
"less": {
"autoPrefix": "",
"cssComb": "none",
"ieCompat": true,
"math": null,
"strictMath": false,
"strictUnits": false,
"relativeUrls": true,
"rootPath": "",
"sourceMapRoot": "",
"sourceMapBasePath": "",
"sourceMap": false
},
"sass": {
"autoPrefix": "",
"loadPaths": "",
"style": "expanded",
"relativeUrls": true,
"sourceMap": false
},
"stylus": {
"sourceMap": false
},
"babel": {
"sourceMap": false
},
"coffeescript": {
"bare": false,
"runtimeMode": "node",
"sourceMap": false
},
"handlebars": {
"root": "",
"noBOM": false,
"name": "",
"namespace": "",
"knownHelpersOnly": false,
"forcePartial": false,
"knownHelpers": [],
"commonjs": "",
"amd": false,
"sourceMap": false
}
},
"minifiers": {
"css": {
"enabled": true,
"termSemicolons": true,
"gzip": false
},
"javascript": {
"enabled": true,
"termSemicolons": true,
"gzip": false
}
}
}
+32
View File
@@ -0,0 +1,32 @@
param([string]$ProjectDir, [string]$ProjectPath);
$FileMajMin = "..\MajMin.vers"
$FileVers = "Resources\VersNum.txt"
$FileManIn = "Resources\manifest-original.xml"
$FileManOut = "Resources\manifest.xml"
$FileCLogIn = "Resources\ChangeLog-original.html"
$FileCLogOut = "Resources\ChangeLog.html"
$MajMin = Get-Content $FileMajMin
$currentDate = get-date -format yyMM;
$currentTime = get-date -format dHH;
$find = "<Version>(.|\n)*?</Version>";
$currRelNum = $MajMin + $currentDate +"." + $currentTime
$replace = "<Version>" + $MajMin + $currentDate +"." + $currentTime + "</Version>";
$csproj = Get-Content $ProjectPath
$csprojUpdated = $csproj -replace $find, $replace
Set-Content -Path $ProjectPath -Value $csprojUpdated
Set-Content -Path $FileVers -Value $currRelNum
# replace x manifest
$manData = Get-Content $FileManIn
$manData = $manData -replace "1.0.0.0", $currRelNum
$manData = $manData -replace "{{DIRNAME}}", "MP-IOC"
$manData = $manData -replace "{{BRANCHNAME}}", "stable/LAST"
$manData = $manData -replace "{{PACKNAME}}", "MP.IOC"
Set-Content -Path $FileManOut -Value $manData
# replace x ChangeLog
$clogData = Get-Content $FileCLogIn
$clogData = $clogData -replace "{{CURRENT-REL}}", $currRelNum
Set-Content -Path $FileCLogOut -Value $clogData
+12 -1
View File
@@ -35,7 +35,18 @@ logger.Info("Setup REDIS OK");
// YARP base config
builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"))
.ConfigureHttpClient((context, handler) =>
{
// Se sei in sviluppo o sul server con problemi di certificato
handler.SslOptions.RemoteCertificateValidationCallback = (sender, cert, chain, sslPolicyErrors) =>
{
// ATTENZIONE: In produzione filtra per hostname o usa cautela
return true;
};
});
builder.Services.AddHttpForwarder();
// HttpMessageInvoker (SocketsHttpHandler)
+9
View File
@@ -2,6 +2,7 @@
"Logging": {
"LogLevel": {
"Default": "Information",
"Yarp": "Debug",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"Microsoft.EntityFrameworkCore.Infrastructure": "Warning",
@@ -64,9 +65,17 @@
],
"Clusters": {
"cluster-old": {
"HttpRequest": {
"Version": "1.1",
"VersionPolicy": "RequestVersionExact"
},
"Destinations": { "old1": { "Address": "https://iis01.egalware.com/MP/IO/IOB/" } }
},
"cluster-new": {
"HttpRequest": {
"Version": "1.1",
"VersionPolicy": "RequestVersionExact"
},
"Destinations": { "new1": { "Address": "https://iis01.egalware.com/MP/IOC/api/IOB/" } }
}
}