Aggiunta progetto API Routing semplificata (solo routing)
This commit is contained in:
@@ -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
@@ -0,0 +1,6 @@
|
||||
@MP_RIOC_HostAddress = http://localhost:5290
|
||||
|
||||
GET {{MP_RIOC_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
@@ -0,0 +1,4 @@
|
||||
<Solution>
|
||||
<Project Path="../MP.Core/MP.Core.csproj" />
|
||||
<Project Path="MP.RIOC.csproj" />
|
||||
</Solution>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,12 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace MP.RIOC.Pages
|
||||
{
|
||||
public class IndexModel : PageModel
|
||||
{
|
||||
public void OnGet()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"outputFile": "Components/Layout/MainLayout.razor.css",
|
||||
"inputFile": "Components/Layout/MainLayout.razor.less"
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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/" } }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user