chore(auth): upgrade OpenIddict to 7.2.0

This commit is contained in:
Sam 2026-02-01 23:24:47 +08:00
commit 1be6309567
179 changed files with 9922 additions and 0 deletions

View File

@ -0,0 +1,99 @@
using Yarp.ReverseProxy.Configuration;
using Microsoft.EntityFrameworkCore;
using System.Collections.Concurrent;
using YarpGateway.Data;
using YarpGateway.Models;
namespace YarpGateway.Config;
public class DatabaseClusterConfigProvider
{
private readonly IDbContextFactory<GatewayDbContext> _dbContextFactory;
private readonly ConcurrentDictionary<string, ClusterConfig> _clusters = new();
private readonly SemaphoreSlim _lock = new(1, 1);
private readonly ILogger<DatabaseClusterConfigProvider> _logger;
public DatabaseClusterConfigProvider(IDbContextFactory<GatewayDbContext> dbContextFactory, ILogger<DatabaseClusterConfigProvider> logger)
{
_dbContextFactory = dbContextFactory;
_logger = logger;
_ = LoadConfigAsync();
}
public IReadOnlyList<ClusterConfig> GetClusters()
{
return _clusters.Values.ToList().AsReadOnly();
}
public async Task ReloadAsync()
{
await _lock.WaitAsync();
try
{
await LoadConfigInternalAsync();
}
finally
{
_lock.Release();
}
}
private async Task LoadConfigAsync()
{
await LoadConfigInternalAsync();
}
private async Task LoadConfigInternalAsync()
{
await using var dbContext = _dbContextFactory.CreateDbContext();
var instances = await dbContext.ServiceInstances
.Where(i => i.Status == 1 && !i.IsDeleted)
.GroupBy(i => i.ClusterId)
.ToListAsync();
var newClusters = new ConcurrentDictionary<string, ClusterConfig>();
foreach (var group in instances)
{
var destinations = new Dictionary<string, DestinationConfig>();
foreach (var instance in group)
{
destinations[instance.DestinationId] = new DestinationConfig
{
Address = instance.Address,
Metadata = new Dictionary<string, string>
{
["Weight"] = instance.Weight.ToString()
}
};
}
var config = new ClusterConfig
{
ClusterId = group.Key,
Destinations = destinations,
LoadBalancingPolicy = "DistributedWeightedRoundRobin",
HealthCheck = new HealthCheckConfig
{
Active = new ActiveHealthCheckConfig
{
Enabled = true,
Interval = TimeSpan.FromSeconds(30),
Timeout = TimeSpan.FromSeconds(5),
Path = "/health"
}
}
};
newClusters[group.Key] = config;
}
_clusters.Clear();
foreach (var cluster in newClusters)
{
_clusters[cluster.Key] = cluster.Value;
}
_logger.LogInformation("Loaded {Count} clusters from database", _clusters.Count);
}
}

View File

@ -0,0 +1,83 @@
using System.Collections.Concurrent;
using Microsoft.EntityFrameworkCore;
using Yarp.ReverseProxy.Configuration;
using YarpGateway.Data;
using YarpGateway.Models;
namespace YarpGateway.Config;
public class DatabaseRouteConfigProvider
{
private readonly IDbContextFactory<GatewayDbContext> _dbContextFactory;
private readonly ConcurrentDictionary<string, RouteConfig> _routes = new();
private readonly SemaphoreSlim _lock = new(1, 1);
private readonly ILogger<DatabaseRouteConfigProvider> _logger;
public DatabaseRouteConfigProvider(
IDbContextFactory<GatewayDbContext> dbContextFactory,
ILogger<DatabaseRouteConfigProvider> logger
)
{
_dbContextFactory = dbContextFactory;
_logger = logger;
_ = LoadConfigAsync();
}
public IReadOnlyList<RouteConfig> GetRoutes()
{
return _routes.Values.ToList().AsReadOnly();
}
public async Task ReloadAsync()
{
await _lock.WaitAsync();
try
{
await LoadConfigInternalAsync();
}
finally
{
_lock.Release();
}
}
private async Task LoadConfigAsync()
{
await LoadConfigInternalAsync();
}
private async Task LoadConfigInternalAsync()
{
await using var dbContext = _dbContextFactory.CreateDbContext();
var routes = await dbContext
.TenantRoutes.Where(r => r.Status == 1 && !r.IsDeleted)
.ToListAsync();
var newRoutes = new ConcurrentDictionary<string, RouteConfig>();
foreach (var route in routes)
{
var config = new RouteConfig
{
RouteId = route.Id.ToString(),
ClusterId = route.ClusterId,
Match = new RouteMatch { Path = route.PathPattern },
Metadata = new Dictionary<string, string>
{
["TenantCode"] = route.TenantCode,
["ServiceName"] = route.ServiceName,
},
};
newRoutes[route.Id.ToString()] = config;
}
_routes.Clear();
foreach (var route in newRoutes)
{
_routes[route.Key] = route.Value;
}
_logger.LogInformation("Loaded {Count} routes from database", _routes.Count);
}
}

9
Config/JwtConfig.cs Normal file
View File

@ -0,0 +1,9 @@
namespace YarpGateway.Config;
public class JwtConfig
{
public string Authority { get; set; } = string.Empty;
public string Audience { get; set; } = string.Empty;
public bool ValidateIssuer { get; set; } = true;
public bool ValidateAudience { get; set; } = true;
}

8
Config/RedisConfig.cs Normal file
View File

@ -0,0 +1,8 @@
namespace YarpGateway.Config;
public class RedisConfig
{
public string ConnectionString { get; set; } = "localhost:6379";
public int Database { get; set; } = 0;
public string InstanceName { get; set; } = "YarpGateway";
}

View File

@ -0,0 +1,272 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using YarpGateway.Data;
using YarpGateway.Config;
using YarpGateway.Models;
using YarpGateway.Services;
using Yarp.ReverseProxy.Configuration;
namespace YarpGateway.Controllers;
[ApiController]
[Route("api/gateway")]
public class GatewayConfigController : ControllerBase
{
private readonly IDbContextFactory<GatewayDbContext> _dbContextFactory;
private readonly DatabaseRouteConfigProvider _routeProvider;
private readonly DatabaseClusterConfigProvider _clusterProvider;
private readonly IRouteCache _routeCache;
public GatewayConfigController(
IDbContextFactory<GatewayDbContext> dbContextFactory,
DatabaseRouteConfigProvider routeProvider,
DatabaseClusterConfigProvider clusterProvider,
IRouteCache routeCache)
{
_dbContextFactory = dbContextFactory;
_routeProvider = routeProvider;
_clusterProvider = clusterProvider;
_routeCache = routeCache;
}
[HttpGet("tenants")]
public async Task<IActionResult> GetTenants()
{
await using var db = _dbContextFactory.CreateDbContext();
var tenants = await db.Tenants
.Where(t => !t.IsDeleted)
.ToListAsync();
return Ok(tenants);
}
[HttpPost("tenants")]
public async Task<IActionResult> CreateTenant([FromBody] CreateTenantDto dto)
{
await using var db = _dbContextFactory.CreateDbContext();
var existing = await db.Tenants
.FirstOrDefaultAsync(t => t.TenantCode == dto.TenantCode);
if (existing != null)
{
return BadRequest($"Tenant code {dto.TenantCode} already exists");
}
var tenant = new GwTenant
{
Id = GenerateId(),
TenantCode = dto.TenantCode,
TenantName = dto.TenantName,
Status = 1
};
await db.Tenants.AddAsync(tenant);
await db.SaveChangesAsync();
return Ok(tenant);
}
[HttpDelete("tenants/{id}")]
public async Task<IActionResult> DeleteTenant(long id)
{
await using var db = _dbContextFactory.CreateDbContext();
var tenant = await db.Tenants.FindAsync(id);
if (tenant == null)
return NotFound();
tenant.IsDeleted = true;
await db.SaveChangesAsync();
return Ok();
}
[HttpGet("tenants/{tenantCode}/routes")]
public async Task<IActionResult> GetTenantRoutes(string tenantCode)
{
await using var db = _dbContextFactory.CreateDbContext();
var routes = await db.TenantRoutes
.Where(r => r.TenantCode == tenantCode && !r.IsDeleted)
.ToListAsync();
return Ok(routes);
}
[HttpPost("tenants/{tenantCode}/routes")]
public async Task<IActionResult> CreateTenantRoute(string tenantCode, [FromBody] CreateTenantRouteDto dto)
{
await using var db = _dbContextFactory.CreateDbContext();
var tenant = await db.Tenants
.FirstOrDefaultAsync(t => t.TenantCode == tenantCode);
if (tenant == null)
return BadRequest($"Tenant {tenantCode} not found");
var clusterId = $"{tenantCode}-{dto.ServiceName}";
var existing = await db.TenantRoutes
.FirstOrDefaultAsync(r => r.ClusterId == clusterId);
if (existing != null)
return BadRequest($"Route for {tenantCode}/{dto.ServiceName} already exists");
var route = new GwTenantRoute
{
Id = GenerateId(),
TenantCode = tenantCode,
ServiceName = dto.ServiceName,
ClusterId = clusterId,
PathPattern = dto.PathPattern,
Priority = 10,
Status = 1,
IsGlobal = false
};
await db.TenantRoutes.AddAsync(route);
await db.SaveChangesAsync();
await _routeCache.ReloadAsync();
return Ok(route);
}
[HttpGet("routes/global")]
public async Task<IActionResult> GetGlobalRoutes()
{
await using var db = _dbContextFactory.CreateDbContext();
var routes = await db.TenantRoutes
.Where(r => r.IsGlobal && !r.IsDeleted)
.ToListAsync();
return Ok(routes);
}
[HttpPost("routes/global")]
public async Task<IActionResult> CreateGlobalRoute([FromBody] CreateGlobalRouteDto dto)
{
await using var db = _dbContextFactory.CreateDbContext();
var existing = await db.TenantRoutes
.FirstOrDefaultAsync(r => r.ServiceName == dto.ServiceName && r.IsGlobal);
if (existing != null)
{
return BadRequest($"Global route for {dto.ServiceName} already exists");
}
var route = new GwTenantRoute
{
Id = GenerateId(),
TenantCode = string.Empty,
ServiceName = dto.ServiceName,
ClusterId = dto.ClusterId,
PathPattern = dto.PathPattern,
Priority = 0,
Status = 1,
IsGlobal = true
};
await db.TenantRoutes.AddAsync(route);
await db.SaveChangesAsync();
await _routeCache.ReloadAsync();
return Ok(route);
}
[HttpDelete("routes/{id}")]
public async Task<IActionResult> DeleteRoute(long id)
{
await using var db = _dbContextFactory.CreateDbContext();
var route = await db.TenantRoutes.FindAsync(id);
if (route == null)
return NotFound();
route.IsDeleted = true;
await db.SaveChangesAsync();
await _routeCache.ReloadAsync();
return Ok();
}
[HttpGet("clusters/{clusterId}/instances")]
public async Task<IActionResult> GetInstances(string clusterId)
{
await using var db = _dbContextFactory.CreateDbContext();
var instances = await db.ServiceInstances
.Where(i => i.ClusterId == clusterId && !i.IsDeleted)
.ToListAsync();
return Ok(instances);
}
[HttpPost("clusters/{clusterId}/instances")]
public async Task<IActionResult> AddInstance(string clusterId, [FromBody] CreateInstanceDto dto)
{
await using var db = _dbContextFactory.CreateDbContext();
var existing = await db.ServiceInstances
.FirstOrDefaultAsync(i => i.ClusterId == clusterId && i.DestinationId == dto.DestinationId);
if (existing != null)
return BadRequest($"Instance {dto.DestinationId} already exists in cluster {clusterId}");
var instance = new GwServiceInstance
{
Id = GenerateId(),
ClusterId = clusterId,
DestinationId = dto.DestinationId,
Address = dto.Address,
Weight = dto.Weight,
Health = 1,
Status = 1
};
await db.ServiceInstances.AddAsync(instance);
await db.SaveChangesAsync();
await _clusterProvider.ReloadAsync();
return Ok(instance);
}
[HttpDelete("instances/{id}")]
public async Task<IActionResult> DeleteInstance(long id)
{
await using var db = _dbContextFactory.CreateDbContext();
var instance = await db.ServiceInstances.FindAsync(id);
if (instance == null)
return NotFound();
instance.IsDeleted = true;
await db.SaveChangesAsync();
await _clusterProvider.ReloadAsync();
return Ok();
}
[HttpPost("reload")]
public async Task<IActionResult> ReloadConfig()
{
await _routeCache.ReloadAsync();
await _routeProvider.ReloadAsync();
await _clusterProvider.ReloadAsync();
return Ok(new { message = "Config reloaded successfully" });
}
private long GenerateId()
{
return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
}
public class CreateTenantDto
{
public string TenantCode { get; set; } = string.Empty;
public string TenantName { get; set; } = string.Empty;
}
public class CreateTenantRouteDto
{
public string ServiceName { get; set; } = string.Empty;
public string PathPattern { get; set; } = string.Empty;
}
public class CreateGlobalRouteDto
{
public string ServiceName { get; set; } = string.Empty;
public string ClusterId { get; set; } = string.Empty;
public string PathPattern { get; set; } = string.Empty;
}
public class CreateInstanceDto
{
public string DestinationId { get; set; } = string.Empty;
public string Address { get; set; } = string.Empty;
public int Weight { get; set; } = 1;
}
}

52
Data/GatewayDbContext.cs Normal file
View File

@ -0,0 +1,52 @@
using Microsoft.EntityFrameworkCore;
using YarpGateway.Models;
namespace YarpGateway.Data;
public class GatewayDbContext : DbContext
{
public GatewayDbContext(DbContextOptions<GatewayDbContext> options)
: base(options)
{
}
public DbSet<GwTenant> Tenants => Set<GwTenant>();
public DbSet<GwTenantRoute> TenantRoutes => Set<GwTenantRoute>();
public DbSet<GwServiceInstance> ServiceInstances => Set<GwServiceInstance>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<GwTenant>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.TenantCode).HasMaxLength(50).IsRequired();
entity.Property(e => e.TenantName).HasMaxLength(100).IsRequired();
entity.HasIndex(e => e.TenantCode).IsUnique();
});
modelBuilder.Entity<GwTenantRoute>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.TenantCode).HasMaxLength(50);
entity.Property(e => e.ServiceName).HasMaxLength(100).IsRequired();
entity.Property(e => e.ClusterId).HasMaxLength(100).IsRequired();
entity.Property(e => e.PathPattern).HasMaxLength(200).IsRequired();
entity.HasIndex(e => e.TenantCode);
entity.HasIndex(e => e.ServiceName);
entity.HasIndex(e => e.ClusterId);
entity.HasIndex(e => new { e.ServiceName, e.IsGlobal, e.Status });
});
modelBuilder.Entity<GwServiceInstance>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.ClusterId).HasMaxLength(100).IsRequired();
entity.Property(e => e.DestinationId).HasMaxLength(100).IsRequired();
entity.Property(e => e.Address).HasMaxLength(200).IsRequired();
entity.HasIndex(e => new { e.ClusterId, e.DestinationId }).IsUnique();
entity.HasIndex(e => e.Health);
});
base.OnModelCreating(modelBuilder);
}
}

View File

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Configuration;
namespace YarpGateway.Data;
public class GatewayDbContextFactory : IDesignTimeDbContextFactory<GatewayDbContext>
{
public GatewayDbContext CreateDbContext(string[] args)
{
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false)
.Build();
var optionsBuilder = new DbContextOptionsBuilder<GatewayDbContext>();
var connectionString = configuration.GetConnectionString("DefaultConnection");
optionsBuilder.UseNpgsql(connectionString);
return new GatewayDbContext(optionsBuilder.Options);
}
}

14
Dockerfile Normal file
View File

@ -0,0 +1,14 @@
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
EXPOSE 8080
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "YarpGateway.dll"]

View File

@ -0,0 +1,69 @@
using Microsoft.Extensions.Primitives;
using Yarp.ReverseProxy.Configuration;
using YarpGateway.Config;
namespace YarpGateway.DynamicProxy;
public class DynamicProxyConfigProvider : IProxyConfigProvider
{
private volatile IProxyConfig _config;
private readonly DatabaseRouteConfigProvider _routeProvider;
private readonly DatabaseClusterConfigProvider _clusterProvider;
private readonly object _lock = new();
public DynamicProxyConfigProvider(
DatabaseRouteConfigProvider routeProvider,
DatabaseClusterConfigProvider clusterProvider)
{
_routeProvider = routeProvider;
_clusterProvider = clusterProvider;
UpdateConfig();
}
public IProxyConfig GetConfig()
{
return _config;
}
public void UpdateConfig()
{
lock (_lock)
{
var routes = _routeProvider.GetRoutes();
var clusters = _clusterProvider.GetClusters();
_config = new InMemoryProxyConfig(
routes,
clusters,
Array.Empty<IReadOnlyDictionary<string, string>>()
);
}
}
public async Task ReloadAsync()
{
await _routeProvider.ReloadAsync();
await _clusterProvider.ReloadAsync();
UpdateConfig();
}
private class InMemoryProxyConfig : IProxyConfig
{
private static readonly CancellationChangeToken _nullChangeToken = new(new CancellationToken());
public InMemoryProxyConfig(
IReadOnlyList<RouteConfig> routes,
IReadOnlyList<ClusterConfig> clusters,
IReadOnlyList<IReadOnlyDictionary<string, string>> transforms)
{
Routes = routes;
Clusters = clusters;
Transforms = transforms;
}
public IReadOnlyList<RouteConfig> Routes { get; }
public IReadOnlyList<ClusterConfig> Clusters { get; }
public IReadOnlyList<IReadOnlyDictionary<string, string>> Transforms { get; }
public IChangeToken ChangeToken => _nullChangeToken;
}
}

View File

@ -0,0 +1,243 @@
using System.Text.Json;
using Microsoft.Extensions.Logging;
using StackExchange.Redis;
using Yarp.ReverseProxy.LoadBalancing;
using Yarp.ReverseProxy.Model;
using YarpGateway.Config;
namespace YarpGateway.LoadBalancing;
public class DistributedWeightedRoundRobinPolicy : ILoadBalancingPolicy
{
private readonly IConnectionMultiplexer _redis;
private readonly RedisConfig _config;
private readonly ILogger<DistributedWeightedRoundRobinPolicy> _logger;
public string Name => "DistributedWeightedRoundRobin";
public DistributedWeightedRoundRobinPolicy(
IConnectionMultiplexer redis,
RedisConfig config,
ILogger<DistributedWeightedRoundRobinPolicy> logger
)
{
_redis = redis;
_config = config;
_logger = logger;
}
public DestinationState? PickDestination(
HttpContext context,
ClusterState cluster,
IReadOnlyList<DestinationState> availableDestinations
)
{
if (availableDestinations.Count == 0)
return null;
if (availableDestinations.Count == 1)
return availableDestinations[0];
var clusterId = cluster.ClusterId;
var db = _redis.GetDatabase();
var lockKey = $"lock:{_config.InstanceName}:{clusterId}";
var stateKey = $"lb:{_config.InstanceName}:{clusterId}:state";
var lockValue = Guid.NewGuid().ToString();
var lockAcquired = db.StringSet(
lockKey,
lockValue,
TimeSpan.FromMilliseconds(500),
When.NotExists
);
if (!lockAcquired)
{
_logger.LogDebug(
"Lock busy for cluster {Cluster}, using fallback selection",
clusterId
);
return FallbackSelection(availableDestinations);
}
try
{
var state = GetOrCreateLoadBalancingState(db, stateKey, availableDestinations);
var selectedDestination = SelectByWeight(state, availableDestinations);
UpdateCurrentWeights(db, stateKey, state, selectedDestination);
_logger.LogDebug(
"Selected {Destination} for cluster {Cluster}",
selectedDestination?.DestinationId,
clusterId
);
return selectedDestination;
}
catch (Exception ex)
{
_logger.LogError(
ex,
"Error in distributed load balancing for cluster {Cluster}",
clusterId
);
return availableDestinations[0];
}
finally
{
var script =
@"
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end";
db.ScriptEvaluate(script, new RedisKey[] { lockKey }, new RedisValue[] { lockValue });
}
}
private LoadBalancingState GetOrCreateLoadBalancingState(
IDatabase db,
string stateKey,
IReadOnlyList<DestinationState> destinations
)
{
var existingState = db.StringGet(stateKey);
if (existingState.HasValue)
{
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
;
var parsedState = (LoadBalancingState?)
System.Text.Json.JsonSerializer.Deserialize<LoadBalancingState>(
existingState.ToString(),
options
);
var version = ComputeConfigHash(destinations);
if (
parsedState != null
&& parsedState.ConfigHash == version
&& parsedState.CurrentWeights != null
)
{
return parsedState;
}
}
var newState = new LoadBalancingState
{
ConfigHash = ComputeConfigHash(destinations),
CurrentWeights = new Dictionary<string, int>(),
};
foreach (var dest in destinations)
{
var weight = GetWeight(dest);
newState.CurrentWeights[dest.DestinationId] = 0;
}
var json = System.Text.Json.JsonSerializer.Serialize(newState);
db.StringSet(stateKey, json, TimeSpan.FromHours(1));
return newState;
}
private long ComputeConfigHash(IReadOnlyList<DestinationState> destinations)
{
var hash = 0L;
foreach (var dest in destinations.OrderBy(d => d.DestinationId))
{
var weight = GetWeight(dest);
hash = HashCode.Combine(hash, dest.DestinationId.GetHashCode());
hash = HashCode.Combine(hash, weight);
}
return hash;
}
private void UpdateCurrentWeights(
IDatabase db,
string stateKey,
LoadBalancingState state,
DestinationState? selected
)
{
if (selected == null)
return;
var json = JsonSerializer.Serialize(state);
db.StringSet(stateKey, json, TimeSpan.FromHours(1));
}
private DestinationState? SelectByWeight(
LoadBalancingState state,
IReadOnlyList<DestinationState> destinations
)
{
int maxWeight = int.MinValue;
int totalWeight = 0;
DestinationState? selected = null;
foreach (var dest in destinations)
{
if (!state.CurrentWeights.ContainsKey(dest.DestinationId))
{
state.CurrentWeights[dest.DestinationId] = 0;
}
var weight = GetWeight(dest);
var currentWeight = state.CurrentWeights[dest.DestinationId];
var newWeight = currentWeight + weight;
state.CurrentWeights[dest.DestinationId] = newWeight;
totalWeight += weight;
if (newWeight > maxWeight)
{
maxWeight = newWeight;
selected = dest;
}
}
if (selected != null)
{
state.CurrentWeights[selected.DestinationId] = maxWeight - totalWeight;
}
return selected;
}
private DestinationState? FallbackSelection(IReadOnlyList<DestinationState> destinations)
{
var hash = ComputeRequestHash();
var index = Math.Abs(hash % destinations.Count);
return destinations[index];
}
private int ComputeRequestHash()
{
var now = DateTime.UtcNow;
return HashCode.Combine(now.Second.GetHashCode(), now.Millisecond.GetHashCode());
}
private int GetWeight(DestinationState destination)
{
if (
destination.Model?.Config?.Metadata?.TryGetValue("Weight", out var weightStr) == true
&& int.TryParse(weightStr, out var weight)
)
{
return weight;
}
return 1;
}
private class LoadBalancingState
{
public long ConfigHash { get; set; }
public Dictionary<string, int> CurrentWeights { get; set; } = new();
}
}

30
Metrics/GatewayMetrics.cs Normal file
View File

@ -0,0 +1,30 @@
using System.Diagnostics.Metrics;
namespace YarpGateway.Metrics;
public class GatewayMetrics
{
private readonly Counter<int> _requestsTotal;
private readonly Histogram<double> _requestDuration;
public GatewayMetrics(IMeterFactory meterFactory)
{
var meter = meterFactory.Create("fengling.gateway");
_requestsTotal = meter.CreateCounter<int>(
"gateway_requests_total",
"Total number of requests");
_requestDuration = meter.CreateHistogram<double>(
"gateway_request_duration_seconds",
"Request duration in seconds");
}
public void RecordRequest(string tenant, string service, int statusCode, double duration)
{
var tag = new KeyValuePair<string, object?>("tenant", tenant);
var tag2 = new KeyValuePair<string, object?>("service", service);
var tag3 = new KeyValuePair<string, object?>("status", statusCode.ToString());
_requestsTotal.Add(1, tag, tag2, tag3);
_requestDuration.Record(duration, tag, tag2);
}
}

View File

@ -0,0 +1,83 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Options;
using YarpGateway.Config;
namespace YarpGateway.Middleware;
public class JwtTransformMiddleware
{
private readonly RequestDelegate _next;
private readonly JwtConfig _jwtConfig;
private readonly ILogger<JwtTransformMiddleware> _logger;
public JwtTransformMiddleware(
RequestDelegate next,
IOptions<JwtConfig> jwtConfig,
ILogger<JwtTransformMiddleware> logger
)
{
_next = next;
_jwtConfig = jwtConfig.Value;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var authHeader = context.Request.Headers["Authorization"].FirstOrDefault();
if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer "))
{
await _next(context);
return;
}
var token = authHeader.Substring("Bearer ".Length).Trim();
try
{
var jwtHandler = new JwtSecurityTokenHandler();
var jwtToken = jwtHandler.ReadJwtToken(token);
var tenantId = jwtToken.Claims.FirstOrDefault(c => c.Type == "tenant")?.Value;
var userId = jwtToken
.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)
?.Value;
var userName = jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value;
var roles = jwtToken
.Claims.Where(c => c.Type == ClaimTypes.Role)
.Select(c => c.Value)
.ToList();
if (!string.IsNullOrEmpty(tenantId))
{
context.Request.Headers["X-Tenant-Id"] = tenantId;
if (!string.IsNullOrEmpty(userId))
context.Request.Headers["X-User-Id"] = userId;
if (!string.IsNullOrEmpty(userName))
context.Request.Headers["X-User-Name"] = userName;
if (roles.Any())
context.Request.Headers["X-Roles"] = string.Join(",", roles);
_logger.LogInformation(
"JWT transformed - Tenant: {Tenant}, User: {User}",
tenantId,
userId
);
}
else
{
_logger.LogWarning("JWT missing tenant claim");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to parse JWT token");
}
await _next(context);
}
}

View File

@ -0,0 +1,63 @@
using Microsoft.Extensions.Options;
using System.Text.RegularExpressions;
using YarpGateway.Services;
namespace YarpGateway.Middleware;
public class TenantRoutingMiddleware
{
private readonly RequestDelegate _next;
private readonly IRouteCache _routeCache;
private readonly ILogger<TenantRoutingMiddleware> _logger;
public TenantRoutingMiddleware(
RequestDelegate next,
IRouteCache routeCache,
ILogger<TenantRoutingMiddleware> logger)
{
_next = next;
_routeCache = routeCache;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault();
if (string.IsNullOrEmpty(tenantId))
{
await _next(context);
return;
}
var path = context.Request.Path.Value ?? string.Empty;
var serviceName = ExtractServiceName(path);
if (string.IsNullOrEmpty(serviceName))
{
await _next(context);
return;
}
var route = _routeCache.GetRoute(tenantId, serviceName);
if (route == null)
{
_logger.LogWarning("Route not found - Tenant: {Tenant}, Service: {Service}", tenantId, serviceName);
await _next(context);
return;
}
context.Items["DynamicClusterId"] = route.ClusterId;
var routeType = route.IsGlobal ? "global" : "tenant-specific";
_logger.LogInformation("Tenant routing - Tenant: {Tenant}, Service: {Service}, Cluster: {Cluster}, Type: {Type}",
tenantId, serviceName, route.ClusterId, routeType);
await _next(context);
}
private string ExtractServiceName(string path)
{
var match = Regex.Match(path, @"/api/(\w+)/?");
return match.Success ? match.Groups[1].Value : string.Empty;
}
}

View File

@ -0,0 +1,209 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using YarpGateway.Data;
#nullable disable
namespace YarpGateway.Migrations
{
[DbContext(typeof(GatewayDbContext))]
[Migration("20260201120312_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("YarpGateway.Models.GwServiceInstance", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("Address")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("ClusterId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<long?>("CreatedBy")
.HasColumnType("bigint");
b.Property<DateTime>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("DestinationId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<int>("Health")
.HasColumnType("integer");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<long?>("UpdatedBy")
.HasColumnType("bigint");
b.Property<DateTime?>("UpdatedTime")
.HasColumnType("timestamp with time zone");
b.Property<int>("Version")
.HasColumnType("integer");
b.Property<int>("Weight")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("Health");
b.HasIndex("ClusterId", "DestinationId")
.IsUnique();
b.ToTable("ServiceInstances");
});
modelBuilder.Entity("YarpGateway.Models.GwTenant", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<long?>("CreatedBy")
.HasColumnType("bigint");
b.Property<DateTime>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<string>("TenantCode")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("TenantName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<long?>("UpdatedBy")
.HasColumnType("bigint");
b.Property<DateTime?>("UpdatedTime")
.HasColumnType("timestamp with time zone");
b.Property<int>("Version")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("TenantCode")
.IsUnique();
b.ToTable("Tenants");
});
modelBuilder.Entity("YarpGateway.Models.GwTenantRoute", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("ClusterId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<long?>("CreatedBy")
.HasColumnType("bigint");
b.Property<DateTime>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<string>("PathPattern")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<int>("Priority")
.HasColumnType("integer");
b.Property<string>("ServiceName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<string>("TenantCode")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<long?>("UpdatedBy")
.HasColumnType("bigint");
b.Property<DateTime?>("UpdatedTime")
.HasColumnType("timestamp with time zone");
b.Property<int>("Version")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("ClusterId");
b.HasIndex("TenantCode", "ServiceName")
.IsUnique();
b.ToTable("TenantRoutes");
});
modelBuilder.Entity("YarpGateway.Models.GwTenantRoute", b =>
{
b.HasOne("YarpGateway.Models.GwTenant", null)
.WithMany()
.HasForeignKey("TenantCode")
.HasPrincipalKey("TenantCode")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,133 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace YarpGateway.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ServiceInstances",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
ClusterId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
DestinationId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
Address = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
Health = table.Column<int>(type: "integer", nullable: false),
Weight = table.Column<int>(type: "integer", nullable: false),
Status = table.Column<int>(type: "integer", nullable: false),
CreatedBy = table.Column<long>(type: "bigint", nullable: true),
CreatedTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
UpdatedBy = table.Column<long>(type: "bigint", nullable: true),
UpdatedTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
Version = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ServiceInstances", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Tenants",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
TenantCode = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
TenantName = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
Status = table.Column<int>(type: "integer", nullable: false),
CreatedBy = table.Column<long>(type: "bigint", nullable: true),
CreatedTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
UpdatedBy = table.Column<long>(type: "bigint", nullable: true),
UpdatedTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
Version = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Tenants", x => x.Id);
table.UniqueConstraint("AK_Tenants_TenantCode", x => x.TenantCode);
});
migrationBuilder.CreateTable(
name: "TenantRoutes",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
TenantCode = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
ServiceName = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
ClusterId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
PathPattern = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
Priority = table.Column<int>(type: "integer", nullable: false),
Status = table.Column<int>(type: "integer", nullable: false),
CreatedBy = table.Column<long>(type: "bigint", nullable: true),
CreatedTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
UpdatedBy = table.Column<long>(type: "bigint", nullable: true),
UpdatedTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
Version = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_TenantRoutes", x => x.Id);
table.ForeignKey(
name: "FK_TenantRoutes_Tenants_TenantCode",
column: x => x.TenantCode,
principalTable: "Tenants",
principalColumn: "TenantCode",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_ServiceInstances_ClusterId_DestinationId",
table: "ServiceInstances",
columns: new[] { "ClusterId", "DestinationId" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ServiceInstances_Health",
table: "ServiceInstances",
column: "Health");
migrationBuilder.CreateIndex(
name: "IX_TenantRoutes_ClusterId",
table: "TenantRoutes",
column: "ClusterId");
migrationBuilder.CreateIndex(
name: "IX_TenantRoutes_TenantCode_ServiceName",
table: "TenantRoutes",
columns: new[] { "TenantCode", "ServiceName" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Tenants_TenantCode",
table: "Tenants",
column: "TenantCode",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ServiceInstances");
migrationBuilder.DropTable(
name: "TenantRoutes");
migrationBuilder.DropTable(
name: "Tenants");
}
}
}

View File

@ -0,0 +1,205 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using YarpGateway.Data;
#nullable disable
namespace YarpGateway.Migrations
{
[DbContext(typeof(GatewayDbContext))]
[Migration("20260201133826_AddIsGlobalToTenantRoute")]
partial class AddIsGlobalToTenantRoute
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("YarpGateway.Models.GwServiceInstance", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("Address")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("ClusterId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<long?>("CreatedBy")
.HasColumnType("bigint");
b.Property<DateTime>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("DestinationId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<int>("Health")
.HasColumnType("integer");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<long?>("UpdatedBy")
.HasColumnType("bigint");
b.Property<DateTime?>("UpdatedTime")
.HasColumnType("timestamp with time zone");
b.Property<int>("Version")
.HasColumnType("integer");
b.Property<int>("Weight")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("Health");
b.HasIndex("ClusterId", "DestinationId")
.IsUnique();
b.ToTable("ServiceInstances");
});
modelBuilder.Entity("YarpGateway.Models.GwTenant", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<long?>("CreatedBy")
.HasColumnType("bigint");
b.Property<DateTime>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<string>("TenantCode")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("TenantName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<long?>("UpdatedBy")
.HasColumnType("bigint");
b.Property<DateTime?>("UpdatedTime")
.HasColumnType("timestamp with time zone");
b.Property<int>("Version")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("TenantCode")
.IsUnique();
b.ToTable("Tenants");
});
modelBuilder.Entity("YarpGateway.Models.GwTenantRoute", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("ClusterId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<long?>("CreatedBy")
.HasColumnType("bigint");
b.Property<DateTime>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<bool>("IsGlobal")
.HasColumnType("boolean");
b.Property<string>("PathPattern")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<int>("Priority")
.HasColumnType("integer");
b.Property<string>("ServiceName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<string>("TenantCode")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<long?>("UpdatedBy")
.HasColumnType("bigint");
b.Property<DateTime?>("UpdatedTime")
.HasColumnType("timestamp with time zone");
b.Property<int>("Version")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("ClusterId");
b.HasIndex("ServiceName");
b.HasIndex("TenantCode");
b.HasIndex("ServiceName", "IsGlobal", "Status");
b.ToTable("TenantRoutes");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,87 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace YarpGateway.Migrations
{
/// <inheritdoc />
public partial class AddIsGlobalToTenantRoute : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_TenantRoutes_Tenants_TenantCode",
table: "TenantRoutes");
migrationBuilder.DropUniqueConstraint(
name: "AK_Tenants_TenantCode",
table: "Tenants");
migrationBuilder.DropIndex(
name: "IX_TenantRoutes_TenantCode_ServiceName",
table: "TenantRoutes");
migrationBuilder.AddColumn<bool>(
name: "IsGlobal",
table: "TenantRoutes",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.CreateIndex(
name: "IX_TenantRoutes_ServiceName",
table: "TenantRoutes",
column: "ServiceName");
migrationBuilder.CreateIndex(
name: "IX_TenantRoutes_ServiceName_IsGlobal_Status",
table: "TenantRoutes",
columns: new[] { "ServiceName", "IsGlobal", "Status" });
migrationBuilder.CreateIndex(
name: "IX_TenantRoutes_TenantCode",
table: "TenantRoutes",
column: "TenantCode");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_TenantRoutes_ServiceName",
table: "TenantRoutes");
migrationBuilder.DropIndex(
name: "IX_TenantRoutes_ServiceName_IsGlobal_Status",
table: "TenantRoutes");
migrationBuilder.DropIndex(
name: "IX_TenantRoutes_TenantCode",
table: "TenantRoutes");
migrationBuilder.DropColumn(
name: "IsGlobal",
table: "TenantRoutes");
migrationBuilder.AddUniqueConstraint(
name: "AK_Tenants_TenantCode",
table: "Tenants",
column: "TenantCode");
migrationBuilder.CreateIndex(
name: "IX_TenantRoutes_TenantCode_ServiceName",
table: "TenantRoutes",
columns: new[] { "TenantCode", "ServiceName" },
unique: true);
migrationBuilder.AddForeignKey(
name: "FK_TenantRoutes_Tenants_TenantCode",
table: "TenantRoutes",
column: "TenantCode",
principalTable: "Tenants",
principalColumn: "TenantCode",
onDelete: ReferentialAction.Restrict);
}
}
}

View File

@ -0,0 +1,202 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using YarpGateway.Data;
#nullable disable
namespace YarpGateway.Migrations
{
[DbContext(typeof(GatewayDbContext))]
partial class GatewayDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("YarpGateway.Models.GwServiceInstance", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("Address")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("ClusterId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<long?>("CreatedBy")
.HasColumnType("bigint");
b.Property<DateTime>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<string>("DestinationId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<int>("Health")
.HasColumnType("integer");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<long?>("UpdatedBy")
.HasColumnType("bigint");
b.Property<DateTime?>("UpdatedTime")
.HasColumnType("timestamp with time zone");
b.Property<int>("Version")
.HasColumnType("integer");
b.Property<int>("Weight")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("Health");
b.HasIndex("ClusterId", "DestinationId")
.IsUnique();
b.ToTable("ServiceInstances");
});
modelBuilder.Entity("YarpGateway.Models.GwTenant", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<long?>("CreatedBy")
.HasColumnType("bigint");
b.Property<DateTime>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<string>("TenantCode")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("TenantName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<long?>("UpdatedBy")
.HasColumnType("bigint");
b.Property<DateTime?>("UpdatedTime")
.HasColumnType("timestamp with time zone");
b.Property<int>("Version")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("TenantCode")
.IsUnique();
b.ToTable("Tenants");
});
modelBuilder.Entity("YarpGateway.Models.GwTenantRoute", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("ClusterId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<long?>("CreatedBy")
.HasColumnType("bigint");
b.Property<DateTime>("CreatedTime")
.HasColumnType("timestamp with time zone");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<bool>("IsGlobal")
.HasColumnType("boolean");
b.Property<string>("PathPattern")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<int>("Priority")
.HasColumnType("integer");
b.Property<string>("ServiceName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<string>("TenantCode")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<long?>("UpdatedBy")
.HasColumnType("bigint");
b.Property<DateTime?>("UpdatedTime")
.HasColumnType("timestamp with time zone");
b.Property<int>("Version")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("ClusterId");
b.HasIndex("ServiceName");
b.HasIndex("TenantCode");
b.HasIndex("ServiceName", "IsGlobal", "Status");
b.ToTable("TenantRoutes");
});
#pragma warning restore 612, 618
}
}
}

89
Migrations/script.sql Normal file
View File

@ -0,0 +1,89 @@
CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
"MigrationId" character varying(150) NOT NULL,
"ProductVersion" character varying(32) NOT NULL,
CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId")
);
START TRANSACTION;
CREATE TABLE "ServiceInstances" (
"Id" bigint GENERATED BY DEFAULT AS IDENTITY,
"ClusterId" character varying(100) NOT NULL,
"DestinationId" character varying(100) NOT NULL,
"Address" character varying(200) NOT NULL,
"Health" integer NOT NULL,
"Weight" integer NOT NULL,
"Status" integer NOT NULL,
"CreatedBy" bigint,
"CreatedTime" timestamp with time zone NOT NULL,
"UpdatedBy" bigint,
"UpdatedTime" timestamp with time zone,
"IsDeleted" boolean NOT NULL,
"Version" integer NOT NULL,
CONSTRAINT "PK_ServiceInstances" PRIMARY KEY ("Id")
);
CREATE TABLE "Tenants" (
"Id" bigint GENERATED BY DEFAULT AS IDENTITY,
"TenantCode" character varying(50) NOT NULL,
"TenantName" character varying(100) NOT NULL,
"Status" integer NOT NULL,
"CreatedBy" bigint,
"CreatedTime" timestamp with time zone NOT NULL,
"UpdatedBy" bigint,
"UpdatedTime" timestamp with time zone,
"IsDeleted" boolean NOT NULL,
"Version" integer NOT NULL,
CONSTRAINT "PK_Tenants" PRIMARY KEY ("Id"),
CONSTRAINT "AK_Tenants_TenantCode" UNIQUE ("TenantCode")
);
CREATE TABLE "TenantRoutes" (
"Id" bigint GENERATED BY DEFAULT AS IDENTITY,
"TenantCode" character varying(50) NOT NULL,
"ServiceName" character varying(100) NOT NULL,
"ClusterId" character varying(100) NOT NULL,
"PathPattern" character varying(200) NOT NULL,
"Priority" integer NOT NULL,
"Status" integer NOT NULL,
"CreatedBy" bigint,
"CreatedTime" timestamp with time zone NOT NULL,
"UpdatedBy" bigint,
"UpdatedTime" timestamp with time zone,
"IsDeleted" boolean NOT NULL,
"Version" integer NOT NULL,
CONSTRAINT "PK_TenantRoutes" PRIMARY KEY ("Id"),
CONSTRAINT "FK_TenantRoutes_Tenants_TenantCode" FOREIGN KEY ("TenantCode") REFERENCES "Tenants" ("TenantCode") ON DELETE RESTRICT
);
CREATE UNIQUE INDEX "IX_ServiceInstances_ClusterId_DestinationId" ON "ServiceInstances" ("ClusterId", "DestinationId");
CREATE INDEX "IX_ServiceInstances_Health" ON "ServiceInstances" ("Health");
CREATE INDEX "IX_TenantRoutes_ClusterId" ON "TenantRoutes" ("ClusterId");
CREATE UNIQUE INDEX "IX_TenantRoutes_TenantCode_ServiceName" ON "TenantRoutes" ("TenantCode", "ServiceName");
CREATE UNIQUE INDEX "IX_Tenants_TenantCode" ON "Tenants" ("TenantCode");
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20260201120312_InitialCreate', '9.0.0');
ALTER TABLE "TenantRoutes" DROP CONSTRAINT "FK_TenantRoutes_Tenants_TenantCode";
ALTER TABLE "Tenants" DROP CONSTRAINT "AK_Tenants_TenantCode";
DROP INDEX "IX_TenantRoutes_TenantCode_ServiceName";
ALTER TABLE "TenantRoutes" ADD "IsGlobal" boolean NOT NULL DEFAULT FALSE;
CREATE INDEX "IX_TenantRoutes_ServiceName" ON "TenantRoutes" ("ServiceName");
CREATE INDEX "IX_TenantRoutes_ServiceName_IsGlobal_Status" ON "TenantRoutes" ("ServiceName", "IsGlobal", "Status");
CREATE INDEX "IX_TenantRoutes_TenantCode" ON "TenantRoutes" ("TenantCode");
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20260201133826_AddIsGlobalToTenantRoute', '9.0.0');
COMMIT;

View File

@ -0,0 +1,18 @@
namespace YarpGateway.Models;
public class GwServiceInstance
{
public long Id { get; set; }
public string ClusterId { get; set; } = string.Empty;
public string DestinationId { get; set; } = string.Empty;
public string Address { get; set; } = string.Empty;
public int Health { get; set; } = 1;
public int Weight { get; set; } = 1;
public int Status { get; set; } = 1;
public long? CreatedBy { get; set; }
public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
public long? UpdatedBy { get; set; }
public DateTime? UpdatedTime { get; set; }
public bool IsDeleted { get; set; } = false;
public int Version { get; set; } = 0;
}

15
Models/GwTenant.cs Normal file
View File

@ -0,0 +1,15 @@
namespace YarpGateway.Models;
public class GwTenant
{
public long Id { get; set; }
public string TenantCode { get; set; } = string.Empty;
public string TenantName { get; set; } = string.Empty;
public int Status { get; set; } = 1;
public long? CreatedBy { get; set; }
public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
public long? UpdatedBy { get; set; }
public DateTime? UpdatedTime { get; set; }
public bool IsDeleted { get; set; } = false;
public int Version { get; set; } = 0;
}

19
Models/GwTenantRoute.cs Normal file
View File

@ -0,0 +1,19 @@
namespace YarpGateway.Models;
public class GwTenantRoute
{
public long Id { get; set; }
public string TenantCode { get; set; } = string.Empty;
public string ServiceName { get; set; } = string.Empty;
public string ClusterId { get; set; } = string.Empty;
public string PathPattern { get; set; } = string.Empty;
public int Priority { get; set; } = 0;
public int Status { get; set; } = 1;
public bool IsGlobal { get; set; } = false;
public long? CreatedBy { get; set; }
public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
public long? UpdatedBy { get; set; }
public DateTime? UpdatedTime { get; set; }
public bool IsDeleted { get; set; } = false;
public int Version { get; set; } = 0;
}

113
Program.cs Normal file
View File

@ -0,0 +1,113 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Serilog;
using Yarp.ReverseProxy.Configuration;
using Yarp.ReverseProxy.LoadBalancing;
using YarpGateway.Config;
using YarpGateway.Data;
using YarpGateway.DynamicProxy;
using YarpGateway.LoadBalancing;
using YarpGateway.Middleware;
using YarpGateway.Services;
using StackExchange.Redis;
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog(
(context, services, configuration) =>
configuration
.ReadFrom.Configuration(context.Configuration)
.ReadFrom.Services(services)
.Enrich.FromLogContext()
);
builder.Services.Configure<JwtConfig>(builder.Configuration.GetSection("Jwt"));
builder.Services.Configure<RedisConfig>(builder.Configuration.GetSection("Redis"));
builder.Services.AddSingleton(sp => sp.GetRequiredService<IOptions<RedisConfig>>().Value);
builder.Services.AddDbContextFactory<GatewayDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))
);
builder.Services.AddSingleton<DatabaseRouteConfigProvider>();
builder.Services.AddSingleton<DatabaseClusterConfigProvider>();
builder.Services.AddSingleton<IRouteCache, RouteCache>();
builder.Services.AddSingleton<IRedisConnectionManager, RedisConnectionManager>();
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
{
var config = sp.GetRequiredService<RedisConfig>();
var connectionOptions = ConfigurationOptions.Parse(config.ConnectionString);
connectionOptions.AbortOnConnectFail = false;
connectionOptions.ConnectRetry = 3;
connectionOptions.ConnectTimeout = 5000;
connectionOptions.SyncTimeout = 3000;
connectionOptions.DefaultDatabase = config.Database;
var connection = ConnectionMultiplexer.Connect(connectionOptions);
connection.ConnectionFailed += (sender, e) =>
{
Serilog.Log.Error(e.Exception, "Redis connection failed");
};
connection.ConnectionRestored += (sender, e) =>
{
Serilog.Log.Information("Redis connection restored");
};
return connection;
});
builder.Services.AddSingleton<ILoadBalancingPolicy, DistributedWeightedRoundRobinPolicy>();
builder.Services.AddSingleton<DynamicProxyConfigProvider>();
builder.Services.AddSingleton<IProxyConfigProvider>(sp => sp.GetRequiredService<DynamicProxyConfigProvider>());
var corsSettings = builder.Configuration.GetSection("Cors");
builder.Services.AddCors(options =>
{
var allowAnyOrigin = corsSettings.GetValue<bool>("AllowAnyOrigin");
var allowedOrigins = corsSettings.GetSection("AllowedOrigins").Get<string[]>() ?? Array.Empty<string>();
options.AddPolicy("AllowFrontend", policy =>
{
if (allowAnyOrigin)
{
policy.AllowAnyOrigin();
}
else
{
policy.WithOrigins(allowedOrigins);
}
policy.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
builder.Services.AddControllers();
var app = builder.Build();
app.UseCors("AllowFrontend");
app.UseMiddleware<JwtTransformMiddleware>();
app.UseMiddleware<TenantRoutingMiddleware>();
app.MapControllers();
app.MapReverseProxy();
await app.Services.GetRequiredService<IRouteCache>().InitializeAsync();
try
{
Log.Information("Starting YARP Gateway");
app.Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "Application terminated unexpectedly");
}
finally
{
Log.CloseAndFlush();
}

View File

@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5046",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,138 @@
using StackExchange.Redis;
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using YarpGateway.Config;
namespace YarpGateway.Services;
public interface IRedisConnectionManager
{
IConnectionMultiplexer GetConnection();
Task<IDisposable> AcquireLockAsync(string key, TimeSpan? expiry = null);
Task<T> ExecuteInLockAsync<T>(string key, Func<Task<T>> func, TimeSpan? expiry = null);
}
public class RedisConnectionManager : IRedisConnectionManager
{
private readonly Lazy<IConnectionMultiplexer> _lazyConnection;
private readonly RedisConfig _config;
private readonly ILogger<RedisConnectionManager> _logger;
public RedisConnectionManager(RedisConfig config, ILogger<RedisConnectionManager> logger)
{
_config = config;
_logger = logger;
_lazyConnection = new Lazy<IConnectionMultiplexer>(() =>
{
var configuration = ConfigurationOptions.Parse(_config.ConnectionString);
configuration.AbortOnConnectFail = false;
configuration.ConnectRetry = 3;
configuration.ConnectTimeout = 5000;
configuration.SyncTimeout = 3000;
configuration.DefaultDatabase = _config.Database;
var connection = ConnectionMultiplexer.Connect(configuration);
connection.ConnectionRestored += (sender, e) =>
{
_logger.LogInformation("Redis connection restored");
};
connection.ConnectionFailed += (sender, e) =>
{
_logger.LogError(e.Exception, "Redis connection failed");
};
_logger.LogInformation("Connected to Redis at {ConnectionString}", _config.ConnectionString);
return connection;
});
}
public IConnectionMultiplexer GetConnection()
{
return _lazyConnection.Value;
}
public async Task<IDisposable> AcquireLockAsync(string key, TimeSpan? expiry = null)
{
var expiryTime = expiry ?? TimeSpan.FromSeconds(10);
var redis = GetConnection();
var db = redis.GetDatabase();
var lockKey = $"lock:{_config.InstanceName}:{key}";
var lockValue = Environment.MachineName + ":" + Process.GetCurrentProcess().Id;
var acquired = await db.StringSetAsync(lockKey, lockValue, expiryTime, When.NotExists);
if (!acquired)
{
var backoff = TimeSpan.FromMilliseconds(100);
var retryCount = 0;
const int maxRetries = 50;
while (!acquired && retryCount < maxRetries)
{
await Task.Delay(backoff);
acquired = await db.StringSetAsync(lockKey, lockValue, expiryTime, When.NotExists);
retryCount++;
if (retryCount < 10)
{
backoff = TimeSpan.FromMilliseconds(100 * (retryCount + 1));
}
}
if (!acquired)
{
throw new TimeoutException($"Failed to acquire lock for key: {lockKey}");
}
}
return new RedisLock(db, lockKey, lockValue, _logger);
}
public async Task<T> ExecuteInLockAsync<T>(string key, Func<Task<T>> func, TimeSpan? expiry = null)
{
using var @lock = await AcquireLockAsync(key, expiry);
return await func();
}
private class RedisLock : IDisposable
{
private readonly IDatabase _db;
private readonly string _key;
private readonly string _value;
private readonly ILogger _logger;
private bool _disposed;
public RedisLock(IDatabase db, string key, string value, ILogger logger)
{
_db = db;
_key = key;
_value = value;
_logger = logger;
}
public void Dispose()
{
if (_disposed) return;
try
{
var script = @"
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end";
_db.ScriptEvaluate(script, new RedisKey[] { _key }, new RedisValue[] { _value });
_logger.LogDebug("Released lock for key: {Key}", _key);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to release lock for key: {Key}", _key);
}
finally
{
_disposed = true;
}
}
}
}

138
Services/RouteCache.cs Normal file
View File

@ -0,0 +1,138 @@
using System.Collections.Concurrent;
using YarpGateway.Models;
using YarpGateway.Data;
using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;
namespace YarpGateway.Services;
public class RouteInfo
{
public long Id { get; set; }
public string ClusterId { get; set; } = string.Empty;
public string PathPattern { get; set; } = string.Empty;
public int Priority { get; set; }
public bool IsGlobal { get; set; }
}
public interface IRouteCache
{
Task InitializeAsync();
Task ReloadAsync();
RouteInfo? GetRoute(string tenantCode, string serviceName);
RouteInfo? GetRouteByPath(string path);
}
public class RouteCache : IRouteCache
{
private readonly IDbContextFactory<GatewayDbContext> _dbContextFactory;
private readonly ILogger<RouteCache> _logger;
private readonly ConcurrentDictionary<string, RouteInfo> _globalRoutes = new();
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, RouteInfo>> _tenantRoutes = new();
private readonly ConcurrentDictionary<string, RouteInfo> _pathRoutes = new();
private readonly ReaderWriterLockSlim _lock = new();
public RouteCache(IDbContextFactory<GatewayDbContext> dbContextFactory, ILogger<RouteCache> logger)
{
_dbContextFactory = dbContextFactory;
_logger = logger;
}
public async Task InitializeAsync()
{
_logger.LogInformation("Initializing route cache from database...");
await LoadFromDatabaseAsync();
_logger.LogInformation("Route cache initialized: {GlobalCount} global routes, {TenantCount} tenant routes",
_globalRoutes.Count, _tenantRoutes.Count);
}
public async Task ReloadAsync()
{
_logger.LogInformation("Reloading route cache...");
await LoadFromDatabaseAsync();
_logger.LogInformation("Route cache reloaded");
}
public RouteInfo? GetRoute(string tenantCode, string serviceName)
{
_lock.EnterUpgradeableReadLock();
try
{
// 1. 优先查找租户专用路由
if (_tenantRoutes.TryGetValue(tenantCode, out var tenantRouteMap) &&
tenantRouteMap.TryGetValue(serviceName, out var tenantRoute))
{
_logger.LogDebug("Found tenant-specific route: {Tenant}/{Service} -> {Cluster}",
tenantCode, serviceName, tenantRoute.ClusterId);
return tenantRoute;
}
// 2. 查找全局路由
if (_globalRoutes.TryGetValue(serviceName, out var globalRoute))
{
_logger.LogDebug("Found global route: {Service} -> {Cluster} for tenant {Tenant}",
serviceName, globalRoute.ClusterId, tenantCode);
return globalRoute;
}
// 3. 没找到
_logger.LogWarning("No route found for: {Tenant}/{Service}", tenantCode, serviceName);
return null;
}
finally
{
_lock.ExitUpgradeableReadLock();
}
}
public RouteInfo? GetRouteByPath(string path)
{
return _pathRoutes.TryGetValue(path, out var route) ? route : null;
}
private async Task LoadFromDatabaseAsync()
{
using var db = _dbContextFactory.CreateDbContext();
var routes = await db.TenantRoutes
.Where(r => r.Status == 1 && !r.IsDeleted)
.ToListAsync();
_lock.EnterWriteLock();
try
{
_globalRoutes.Clear();
_tenantRoutes.Clear();
_pathRoutes.Clear();
foreach (var route in routes)
{
var routeInfo = new RouteInfo
{
Id = route.Id,
ClusterId = route.ClusterId,
PathPattern = route.PathPattern,
Priority = route.Priority,
IsGlobal = route.IsGlobal
};
if (route.IsGlobal)
{
_globalRoutes[route.ServiceName] = routeInfo;
_pathRoutes[route.PathPattern] = routeInfo;
}
else if (!string.IsNullOrEmpty(route.TenantCode))
{
_tenantRoutes.GetOrAdd(route.TenantCode, _ => new())
[route.ServiceName] = routeInfo;
_pathRoutes[route.PathPattern] = routeInfo;
}
}
}
finally
{
_lock.ExitWriteLock();
}
}
}

23
YarpGateway.csproj Normal file
View File

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="StackExchange.Redis" Version="2.8.16" />
<PackageReference Include="Yarp.ReverseProxy" Version="2.2.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

64
appsettings.json Normal file
View File

@ -0,0 +1,64 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Yarp.ReverseProxy": "Information"
}
},
"AllowedHosts": "*",
"Cors": {
"AllowedOrigins": [
"http://localhost:5173",
"http://127.0.0.1:5173",
"http://localhost:5174"
],
"AllowAnyOrigin": false
},
"ConnectionStrings": {
"DefaultConnection": "Host=192.168.100.10;Port=5432;Database=fengling_gateway;Username=movingsam;Password=sl52788542"
},
"Jwt": {
"Authority": "https://your-auth-server.com",
"Audience": "fengling-gateway",
"ValidateIssuer": true,
"ValidateAudience": true
},
"Redis": {
"ConnectionString": "192.168.100.10:6379",
"Database": 0,
"InstanceName": "YarpGateway"
},
"ReverseProxy": {
"Routes": {},
"Clusters": {}
},
"Serilog": {
"Using": ["Serilog.Sinks.Console", "Serilog.Sinks.File"],
"MinimumLevel": "Information",
"WriteTo": [
{
"Name": "Console",
"Args": {
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
}
},
{
"Name": "File",
"Args": {
"path": "logs/gateway-.log",
"rollingInterval": "Day",
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
}
}
],
"Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"]
},
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://0.0.0.0:8080"
}
}
}
}

BIN
bin/Debug/net10.0/Humanizer.dll Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
bin/Debug/net10.0/Npgsql.dll Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
bin/Debug/net10.0/Serilog.dll Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
bin/Debug/net10.0/YarpGateway Executable file

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,20 @@
{
"runtimeOptions": {
"tfm": "net10.0",
"frameworks": [
{
"name": "Microsoft.NETCore.App",
"version": "10.0.0"
},
{
"name": "Microsoft.AspNetCore.App",
"version": "10.0.0"
}
],
"configProperties": {
"System.GC.Server": true,
"System.Reflection.NullabilityInfoContext.IsSupported": true,
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
}
}
}

View File

@ -0,0 +1 @@
{"Version":1,"ManifestType":"Build","Endpoints":[]}

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -0,0 +1,64 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Yarp.ReverseProxy": "Information"
}
},
"AllowedHosts": "*",
"Cors": {
"AllowedOrigins": [
"http://localhost:5173",
"http://127.0.0.1:5173",
"http://localhost:5174"
],
"AllowAnyOrigin": false
},
"ConnectionStrings": {
"DefaultConnection": "Host=192.168.100.10;Port=5432;Database=fengling_gateway;Username=movingsam;Password=sl52788542"
},
"Jwt": {
"Authority": "https://your-auth-server.com",
"Audience": "fengling-gateway",
"ValidateIssuer": true,
"ValidateAudience": true
},
"Redis": {
"ConnectionString": "192.168.100.10:6379",
"Database": 0,
"InstanceName": "YarpGateway"
},
"ReverseProxy": {
"Routes": {},
"Clusters": {}
},
"Serilog": {
"Using": ["Serilog.Sinks.Console", "Serilog.Sinks.File"],
"MinimumLevel": "Information",
"WriteTo": [
{
"Name": "Console",
"Args": {
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
}
},
{
"Name": "File",
"Args": {
"path": "logs/gateway-.log",
"rollingInterval": "Day",
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
}
}
],
"Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"]
},
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://0.0.0.0:8080"
}
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More